Browse Source

Merge branch 'dev' into seq-based-roaming

StageGuard 2 years ago
parent
commit
73c6bdeee7
100 changed files with 3207 additions and 1940 deletions
  1. 7 7
      .github/workflows/build.yml
  2. 11 6
      README.md
  3. 2 7
      build.gradle.kts
  4. 11 4
      buildSrc/build.gradle.kts
  5. 16 13
      buildSrc/src/main/kotlin/HmppConfigure.kt
  6. 4 6
      buildSrc/src/main/kotlin/JvmPublishing.kt
  7. 31 0
      buildSrc/src/main/kotlin/LocalProperties.kt
  8. 11 8
      buildSrc/src/main/kotlin/MppPublishing.kt
  9. 2 3
      buildSrc/src/main/kotlin/ProjectConfigure.kt
  10. 8 5
      buildSrc/src/main/kotlin/Relocation.kt
  11. 3 3
      buildSrc/src/main/kotlin/Shadow.kt
  12. 41 0
      buildSrc/src/main/kotlin/TestDependencies.kt
  13. 16 13
      buildSrc/src/main/kotlin/Versions.kt
  14. 5 5
      ci-release-helper/build.gradle.kts
  15. 52 0
      ci-release-helper/changelogs/2.15.0-M1.md
  16. 21 18
      ci-release-helper/src/buildIndex/SnapshotVersions.kt
  17. 5 5
      docs/ConsoleTerminal.md
  18. 2 0
      docs/README.md
  19. 29 5
      docs/UserManual.md
  20. 41 2
      mirai-console/backend/integration-test/testers/service-loader/service-loader-2dep-plugin/src/PMain.kt
  21. 3 1
      mirai-console/backend/mirai-console/build.gradle.kts
  22. 4 2
      mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api
  23. 30 5
      mirai-console/backend/mirai-console/src/MiraiConsole.kt
  24. 2 2
      mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt
  25. 15 0
      mirai-console/backend/mirai-console/src/data/PluginData.kt
  26. 8 0
      mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt
  27. 53 0
      mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt
  28. 53 0
      mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt
  29. 55 17
      mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt
  30. 22 13
      mirai-console/backend/mirai-console/src/internal/data/collectionUtil.kt
  31. 37 6
      mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt
  32. 7 0
      mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginClasspath.kt
  33. 0 157
      mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt
  34. 159 0
      mirai-console/backend/mirai-console/test/data/MultiFilePluginDataStorageImplTests.kt
  35. 2 2
      mirai-console/docs/BuiltInCommands.md
  36. 5 4
      mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts
  37. 2 1
      mirai-console/frontend/mirai-console-terminal/build.gradle.kts
  38. 2 1
      mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt
  39. 2 2
      mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt
  40. 4 3
      mirai-console/tools/intellij-plugin/build.gradle.kts
  41. 3 3
      mirai-console/tools/intellij-plugin/run/projects/test-project/build.gradle.kts
  42. 10 1
      mirai-console/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.properties
  43. 4 2
      mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt
  44. 3 1
      mirai-core-api/build.gradle.kts
  45. 163 106
      mirai-core-api/compatibility-validation/android/api/android.api
  46. 164 106
      mirai-core-api/compatibility-validation/jvm/api/jvm.api
  47. 52 0
      mirai-core-api/src/commonMain/kotlin/BotFactory.kt
  48. 128 0
      mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt
  49. 101 0
      mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt
  50. 9 1
      mirai-core-api/src/commonMain/kotlin/contact/Group.kt
  51. 6 1
      mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt
  52. 70 0
      mirai-core-api/src/commonMain/kotlin/contact/essence/EssenceMessageRecord.kt
  53. 69 0
      mirai-core-api/src/commonMain/kotlin/contact/essence/Essences.kt
  54. 4 4
      mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt
  55. 17 18
      mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt
  56. 1 1
      mirai-core-api/src/commonMain/kotlin/message/data/OfflineMessageSource.kt
  57. 69 9
      mirai-core-api/src/commonMain/kotlin/message/data/OnlineMessageSource.kt
  58. 37 3
      mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt
  59. 38 0
      mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt
  60. 124 57
      mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt
  61. 18 1
      mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt
  62. 22 519
      mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt
  63. 164 7
      mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt
  64. 15 508
      mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt
  65. 2 2
      mirai-core-mock/build.gradle.kts
  66. 9 4
      mirai-core-mock/src/contact/MockGroup.kt
  67. 16 0
      mirai-core-mock/src/contact/active/MockGroupActive.kt
  68. 24 0
      mirai-core-mock/src/contact/essence/MockEssences.kt
  69. 8 0
      mirai-core-mock/src/internal/MockBotFactoryImpl.kt
  70. 2 4
      mirai-core-mock/src/internal/MockMiraiImpl.kt
  71. 4 3
      mirai-core-mock/src/internal/contact/MockAnnouncementsImpl.kt
  72. 1 1
      mirai-core-mock/src/internal/contact/MockFriendImpl.kt
  73. 11 11
      mirai-core-mock/src/internal/contact/MockGroupImpl.kt
  74. 1 1
      mirai-core-mock/src/internal/contact/MockStrangerImpl.kt
  75. 32 4
      mirai-core-mock/src/internal/contact/active/MockGroupActive.kt
  76. 59 0
      mirai-core-mock/src/internal/contact/essence/MockEssences.kt
  77. 46 9
      mirai-core-mock/src/internal/contact/util.kt
  78. 24 3
      mirai-core-mock/src/internal/msgsrc/OnlineMsgSrc.kt
  79. 6 4
      mirai-core-mock/src/internal/remotefile/absolutefile/MockAbsoluteFolder.kt
  80. 2 2
      mirai-core-mock/src/internal/serverfs/MockServerFileDiskImpl.kt
  81. 10 0
      mirai-core-mock/src/internal/serverfs/TmpResourceServerImpl.kt
  82. 5 0
      mirai-core-mock/src/resserver/TmpResourceServer.kt
  83. 34 1
      mirai-core-mock/test/AbsoluteFileTest.kt
  84. 21 0
      mirai-core-mock/test/ImageUploadTest.kt
  85. 2 2
      mirai-core-mock/test/mock/MessageSerializationTest.kt
  86. 2 2
      mirai-core-mock/test/mock/MessagingTest.kt
  87. 104 19
      mirai-core-mock/test/mock/MockGroupTest.kt
  88. 0 76
      mirai-core-utils/src/commonMain/kotlin/IO.kt
  89. 91 0
      mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt
  90. 67 0
      mirai-core-utils/src/commonMain/kotlin/Services.kt
  91. 225 0
      mirai-core-utils/src/commonMain/kotlin/TlvMap.kt
  92. 135 0
      mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TlvMapTest.kt
  93. 14 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Collections.kt
  94. 26 49
      mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt
  95. 77 12
      mirai-core-utils/src/jvmBaseMain/kotlin/Services.kt
  96. 3 3
      mirai-core-utils/src/jvmMain/kotlin/IO.jvm.kt
  97. 2 2
      mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt
  98. 47 0
      mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt
  99. 2 43
      mirai-core-utils/src/nativeMain/kotlin/Service.kt
  100. 19 9
      mirai-core/src/commonMain/kotlin/BotAccount.kt

+ 7 - 7
.github/workflows/build.yml

@@ -58,7 +58,7 @@ jobs:
 
       - run: >
           ./gradlew updateSnapshotVersion ${{ env.gradleArgs }}
-        if: github.event.pusher
+        if: github.event.pusher && vars.RUN_MIRAI_SNAPSHOTS == 'true'
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
           SNAPSHOTS_PUBLISHING_USER: ${{ secrets.SNAPSHOTS_PUBLISHING_USER }}
@@ -213,7 +213,7 @@ jobs:
 
       - run: >
           ./gradlew updateSnapshotVersion ${{ env.gradleArgs }}
-        if: github.event.pusher
+        if: github.event.pusher && vars.RUN_MIRAI_SNAPSHOTS == 'true'
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
           SNAPSHOTS_PUBLISHING_USER: ${{ secrets.SNAPSHOTS_PUBLISHING_USER }}
@@ -240,7 +240,7 @@ jobs:
         run: node ci-release-helper/scripts/kill-java.js
 
       - name: Publish Snapshots
-        if: ${{ github.event.pusher && env.isMac == 'true' }}
+        if: ${{ github.event.pusher && env.isMac == 'true' && vars.RUN_MIRAI_SNAPSHOTS == 'true' }}
         run: ./gradlew publishAllPublicationsToMiraiRepoRepository ${{ env.gradleArgs }}
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
@@ -356,7 +356,7 @@ jobs:
 
       - run: >
           ./gradlew updateSnapshotVersion ${{ env.gradleArgs }}
-        if: github.event.pusher
+        if: github.event.pusher && vars.RUN_MIRAI_SNAPSHOTS == 'true'
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
           SNAPSHOTS_PUBLISHING_USER: ${{ secrets.SNAPSHOTS_PUBLISHING_USER }}
@@ -388,7 +388,7 @@ jobs:
         run: node ci-release-helper/scripts/kill-java.js
 
       - name: Publish MingwX64 Snapshots
-        if: ${{ github.event.pusher && env.isWindows == 'true' }}
+        if: ${{ github.event.pusher && env.isWindows == 'true' && vars.RUN_MIRAI_SNAPSHOTS == 'true' }}
         run: ./gradlew publishMingwX64PublicationToMiraiRepoRepository ${{ env.gradleArgs }}
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
@@ -397,7 +397,7 @@ jobs:
           SNAPSHOTS_PUBLISHING_URL: ${{ secrets.SNAPSHOTS_PUBLISHING_URL }}
 
       - name: Publish LinuxX64 Snapshots
-        if: ${{ github.event.pusher && env.isUbuntu == 'true' }}
+        if: ${{ github.event.pusher && env.isUbuntu == 'true' && vars.RUN_MIRAI_SNAPSHOTS == 'true' }}
         run: ./gradlew publishLinuxX64PublicationToMiraiRepoRepository ${{ env.gradleArgs }}
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true
@@ -406,7 +406,7 @@ jobs:
           SNAPSHOTS_PUBLISHING_URL: ${{ secrets.SNAPSHOTS_PUBLISHING_URL }}
 
       - name: Publish macOSX64 Snapshots
-        if: ${{ github.event.pusher && env.isMac == 'true' }}
+        if: ${{ github.event.pusher && env.isMac == 'true' && vars.RUN_MIRAI_SNAPSHOTS == 'true' }}
         run: ./gradlew publishMacosX64PublicationToMiraiRepoRepository ${{ env.gradleArgs }}
         env:
           MIRAI_IS_SNAPSHOTS_PUBLISHING: true

+ 11 - 6
README.md

@@ -113,18 +113,23 @@ mirai 是一个在全平台下运行,提供 QQ Android 协议支持的高效
 
 **一切开发旨在学习,请勿用于非法用途**
 
-## 开始
+## 快速使用
 
 - **用户手册**: [UserManual](docs/UserManual.md)
-- 开发文档: [在 GitHub 阅读](docs/README.md)
-  或 [在 docs.mirai.mamoe.net 阅读](https://docs.mirai.mamoe.net/)
-- 帮助 mirai: [CONTRIBUTING](docs/contributing/README.md)
+
+  > 如果你希望快速部署一个 Mirai QQ 机器人,安装插件、并投入使用,请看这里
 - 论坛: [Mirai Forum](https://mirai.mamoe.net/)
-  > *Mirai 只有唯一一个官方论坛 Mirai Forum*
 
+  > Mirai 只有**唯一一个**官方论坛 Mirai Forum
+- 在线讨论: [Gitter](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
+
+## 开发相关
+
+- 开发文档: [在 GitHub 阅读](docs/README.md)
+  或 [在 docs.mirai.mamoe.net 阅读](https://docs.mirai.mamoe.net/)
+- 参与贡献: [CONTRIBUTING](docs/contributing/README.md)
 - 更新日志: [release](https://github.com/mamoe/mirai/releases)
 - 开发计划: [milestones](https://github.com/mamoe/mirai/milestones)
-- 在线讨论: [Gitter](https://gitter.im/mamoe/mirai?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
 
 - mirai 开发组和官方系列项目: [project-mirai](https://github.com/project-mirai)
 - mirai 社区相关项目 (

+ 2 - 7
build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -45,6 +45,7 @@ plugins {
 
 osDetector = osdetector
 BuildSrcRootProjectHolder.value = rootProject
+BuildSrcRootProjectHolder.lastUpdateTime = System.currentTimeMillis()
 
 analyzes.CompiledCodeVerify.run { registerAllVerifyTasks() }
 
@@ -70,12 +71,6 @@ allprojects {
         configureKotlinTestSettings()
         configureKotlinExperimentalUsages()
 
-        runCatching {
-            blockingBridge {
-                unitCoercion = me.him188.kotlin.jvm.blocking.bridge.compiler.UnitCoercion.COMPATIBILITY
-            }
-        }
-
         //  useIr()
 
         if (isKotlinJvmProject) {

+ 11 - 4
buildSrc/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -16,7 +16,6 @@ repositories {
     google()
     mavenCentral()
     gradlePluginPortal()
-    maven("https://repo.mirai.mamoe.net/keep") // for modified shadow plugin
 }
 
 kotlin {
@@ -54,14 +53,22 @@ dependencies {
     // api("com.github.jengelman.gradle.plugins", "shadow", version("shadow"))
     api("com.github.johnrengelman", "shadow", version("shadow"))
 
-    api("org.jetbrains.kotlin", "kotlin-gradle-plugin", version("kotlinCompiler"))
+    api("org.jetbrains.kotlin", "kotlin-gradle-plugin", version("kotlinCompiler")) {
+        exclude("org.jetbrains.kotlin", "kotlin-stdlib")
+        exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
+        exclude("org.jetbrains.kotlin", "kotlin-reflect")
+    }
 //    api("org.jetbrains.kotlin", "kotlin-compiler-embeddable", version("kotlinCompiler"))
 //    api(ktor("client-okhttp", "1.4.3"))
     api("com.android.tools.build", "gradle", version("androidGradlePlugin"))
     api(asm("tree"))
     api(asm("util"))
     api(asm("commons"))
-    api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
+    api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") {
+        exclude("org.jetbrains.kotlin", "kotlin-stdlib")
+        exclude("org.jetbrains.kotlin", "kotlin-reflect")
+        exclude("org.jetbrains.kotlin", "kotlin-stdlib-common")
+    }
 
     api("gradle.plugin.com.google.gradle:osdetector-gradle-plugin:1.7.0")
 

+ 16 - 13
buildSrc/src/main/kotlin/HmppConfigure.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -18,6 +18,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPI
 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
 import org.jetbrains.kotlin.gradle.plugin.KotlinTargetPreset
 import org.jetbrains.kotlin.gradle.plugin.mpp.*
 import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
@@ -81,7 +82,7 @@ enum class HostArch {
 
 /// eg. "!a;!b" means to enable all targets but a or b
 /// eg. "a;b;!other" means to disable all targets but a or b
-val ENABLED_TARGETS by lazy {
+val ENABLED_TARGETS by projectLazy {
 
     val targets = getMiraiTargetFromGradle() // enable all by default
 
@@ -116,7 +117,7 @@ fun isTargetEnabled(name: String): Boolean {
 fun Set<String>.filterTargets() =
     this.filter { isTargetEnabled(it) }.toSet()
 
-val MAC_TARGETS: Set<String> by lazy {
+val MAC_TARGETS: Set<String> by projectLazy {
     setOf(
 //        "watchosX86",
         "macosX64",
@@ -141,13 +142,13 @@ val MAC_TARGETS: Set<String> by lazy {
     ).filterTargets()
 }
 
-val WIN_TARGETS by lazy { setOf("mingwX64").filterTargets() }
+val WIN_TARGETS by projectLazy { setOf("mingwX64").filterTargets() }
 
-val LINUX_TARGETS by lazy { setOf("linuxX64").filterTargets() }
+val LINUX_TARGETS by projectLazy { setOf("linuxX64").filterTargets() }
 
-val UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS }
+val UNIX_LIKE_TARGETS by projectLazy { LINUX_TARGETS + MAC_TARGETS }
 
-val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
+val NATIVE_TARGETS by projectLazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
 
 private val POSSIBLE_NATIVE_TARGETS by lazy { setOf("mingwX64", "macosX64", "macosArm64", "linuxX64") }
 
@@ -159,7 +160,9 @@ fun Project.configureJvmTargetsHierarchical() {
         if (IDEA_ACTIVE) {
             jvm("jvmBase") { // dummy target for resolution, not published
                 compilations.all {
-                    this.compileKotlinTask.enabled = false // IDE complain
+                    this.compileTaskProvider.configure { // IDE complain
+                        enabled = false
+                    } 
                 }
                 attributes.attribute(KotlinPlatformType.attribute, KotlinPlatformType.common) // magic
                 attributes.attribute(MIRAI_PLATFORM_ATTRIBUTE, "jvmBase") // avoid resolution
@@ -232,13 +235,13 @@ fun KotlinMultiplatformExtension.configureNativeTargetsHierarchical(
 
     val nativeMainSets = mutableListOf<KotlinSourceSet>()
     val nativeTestSets = mutableListOf<KotlinSourceSet>()
-    val nativeTargets = mutableListOf<KotlinNativeTarget>()
+    val nativeTargets = mutableListOf<KotlinTarget>() // actually KotlinNativeTarget, but KotlinNativeTarget is an internal API (complained by IDEA)
 
 
     fun KotlinMultiplatformExtension.addNativeTarget(
         preset: KotlinTargetPreset<*>,
-    ): KotlinNativeTarget {
-        val target = targetFromPreset(preset, preset.name) as KotlinNativeTarget
+    ): KotlinTarget {
+        val target = targetFromPreset(preset, preset.name) 
         nativeMainSets.add(target.compilations[MAIN_COMPILATION_NAME].kotlinSourceSets.first())
         nativeTestSets.add(target.compilations[TEST_COMPILATION_NAME].kotlinSourceSets.first())
         nativeTargets.add(target)
@@ -313,7 +316,7 @@ fun KotlinMultiplatformExtension.configureNativeTargetsHierarchical(
     }
 
     // Workaround from https://youtrack.jetbrains.com/issue/KT-52433/KotlinNative-Unable-to-generate-framework-with-Kotlin-1621-and-Xcode-134#focus=Comments-27-6140143.0-0
-    project.tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink>().configureEach {
+    project.tasks.withType<KotlinNativeLink>().configureEach {
         val properties = listOf(
             "ios_arm32", "watchos_arm32", "watchos_x86"
         ).joinToString(separator = ";") { "clangDebugFlags.$it=-Os" }
@@ -540,7 +543,7 @@ private fun Project.configureNativeInterop(
             }
         }
 
-        val generateKotlinBindings = tasks.register("generateKotlinBindings${compilationName.titlecase()}") {
+        tasks.register("generateKotlinBindings${compilationName.titlecase()}") {
             group = "mirai"
             description = "Generates Kotlin bindings for Rust"
             dependsOn(bindgen)

+ 4 - 6
buildSrc/src/main/kotlin/JvmPublishing.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -38,11 +38,9 @@ fun Project.configureRemoteRepos() {
         repositories {
             maven {
                 name = "MiraiStageRepo"
-                var stageRepoLoc = getLocalProperty("publishing.stage-repo")?.let(::File)
-                if (stageRepoLoc?.exists() != true) {
-                    stageRepoLoc = rootProject.file("ci-release-helper/stage-repo")
-                }
-                stageRepoLoc as File
+                val stageRepoLoc = getLocalProperty("publishing.stage-repo")?.let(::File)
+                    ?.takeIf { it.exists() }
+                    ?: rootProject.file("ci-release-helper/stage-repo")
 
                 url = stageRepoLoc.also { it.mkdirs() }.toURI()
             }

+ 31 - 0
buildSrc/src/main/kotlin/LocalProperties.kt

@@ -21,10 +21,41 @@ import java.util.*
 
 object BuildSrcRootProjectHolder {
     lateinit var value: Project
+    var lastUpdateTime: Long = 0
 }
 
 val rootProject: Project get() = BuildSrcRootProjectHolder.value
 
+fun <T> projectLazy(action: () -> T): Lazy<T> {
+    val projLazy = object : Lazy<T> {
+        private lateinit var delegate: Lazy<T>
+        private var holdTime: Long = -1
+
+        override val value: T
+            get() {
+                if (holdTime != BuildSrcRootProjectHolder.lastUpdateTime) {
+                    synchronized(this) {
+                        if (holdTime != BuildSrcRootProjectHolder.lastUpdateTime) {
+                            delegate = lazy(action)
+                            holdTime = BuildSrcRootProjectHolder.lastUpdateTime
+                        }
+                    }
+                }
+                return delegate.value
+            }
+
+        override fun isInitialized(): Boolean {
+            if (!::delegate.isInitialized) return false
+
+            if (holdTime == BuildSrcRootProjectHolder.lastUpdateTime) {
+                return delegate.isInitialized()
+            }
+            return false
+        }
+    }
+    return projLazy
+}
+
 
 private lateinit var localProperties: Properties
 

+ 11 - 8
buildSrc/src/main/kotlin/MppPublishing.kt

@@ -16,8 +16,8 @@ import org.gradle.jvm.tasks.Jar
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.register
 
-fun logPublishing(message: String) {
-    println("[Publishing] Configuring $message")
+inline fun logPublishing(@Suppress("UNUSED_PARAMETER") message: () -> String) {
+//    println("[Publishing] Configuring $message")
 }
 
 fun Project.configureMppPublishing() {
@@ -42,7 +42,7 @@ fun Project.configureMppPublishing() {
 
     afterEvaluate {
         publishing {
-            logPublishing("Publications: ${publications.joinToString { it.name }}")
+            logPublishing { "Publications: ${publications.joinToString { it.name }}" }
 
             val (nonJvmPublications, jvmPublications) = publications.filterIsInstance<MavenPublication>()
                 .partition { publication -> tasks.findByName("relocate${publication.name.titlecase()}Dependencies") == null }
@@ -93,7 +93,7 @@ private fun Project.configureMultiplatformPublication(
     publication.artifact(stubJavadoc)
     publication.setupPom(project)
 
-    logPublishing(publication.name + ": moduleName = $moduleName")
+    logPublishing { publication.name + ": moduleName = $moduleName" }
     when (moduleName) {
         "kotlinMultiplatform" -> {
             publication.artifactId = project.name
@@ -103,9 +103,11 @@ private fun Project.configureMultiplatformPublication(
             // TODO: 2021/1/30 现在添加 JVM 到 root module 会导致 Gradle 依赖无法解决
             // https://github.com/mamoe/mirai/issues/932
         }
+
         "metadata" -> { // TODO: 2021/1/21 seems no use. none `type` is "metadata"
             publication.artifactId = "${project.name}-metadata"
         }
+
         else -> {
             // "jvm", "native", "js", "common"
             publication.artifactId = "${project.name}-$moduleName"
@@ -123,12 +125,13 @@ val publishPlatformArtifactsInRootModule: Project.(MavenPublication) -> Unit = {
             // mirai-core\build\libs\mirai-core-2.0.0.jar, classifier=null, ext=jar
         }
 
-        logPublishing("Existing artifacts in kotlinMultiplatform: " +
-                this.artifacts.joinToString("\n", prefix = "\n") { it.smartToString() }
-        )
+        logPublishing {
+            "Existing artifacts in kotlinMultiplatform: " +
+                    this.artifacts.joinToString("\n", prefix = "\n") { it.smartToString() }
+        }
 
         platformPublication.artifacts.forEach {
-            logPublishing("Adding artifact to kotlinMultiplatform: ${it.smartToString()}")
+            logPublishing { "Adding artifact to kotlinMultiplatform: ${it.smartToString()}" }
             artifact(it)
         }
 

+ 2 - 3
buildSrc/src/main/kotlin/ProjectConfigure.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -23,7 +23,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
 import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
-import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
 import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
 import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
 
@@ -250,7 +249,7 @@ val Project.kotlinSourceSets get() = extensions.findByName("kotlin").safeAs<Kotl
 
 val Project.kotlinTargets
     get() =
-        extensions.findByName("kotlin").safeAs<KotlinSingleTargetExtension>()?.target?.let { listOf(it) }
+        extensions.findByName("kotlin").safeAs<KotlinSingleTargetExtension<*>>()?.target?.let { listOf(it) }
             ?: extensions.findByName("kotlin").safeAs<KotlinMultiplatformExtension>()?.targets
 
 val Project.isKotlinJvmProject: Boolean get() = extensions.findByName("kotlin") is KotlinJvmProjectExtension

+ 8 - 5
buildSrc/src/main/kotlin/Relocation.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -101,7 +101,7 @@ fun KotlinDependencyHandler.relocateCompileOnly(
     }
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
         )
     )
     // Don't add to runtime
@@ -125,7 +125,7 @@ fun DependencyHandler.relocateCompileOnly(
         })
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
+            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = false,
         )
     )
     // Don't add to runtime
@@ -149,7 +149,7 @@ fun KotlinDependencyHandler.relocateImplementation(
     }
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
+            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
         )
     )
     project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
@@ -184,7 +184,7 @@ fun DependencyHandler.relocateImplementation(
         })
     project.relocationFilters.add(
         RelocationFilter(
-            dependency.group!!, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
+            dependency.groupNotNull, dependency.name, relocatedDependency.packages.toList(), includeInRuntime = true,
         )
     )
     project.configurations.maybeCreate(SHADOW_RELOCATION_CONFIGURATION_NAME)
@@ -201,6 +201,9 @@ fun DependencyHandler.relocateImplementation(
     return dependency
 }
 
+@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // compiler bug
+private val ExternalModuleDependency.groupNotNull get() = group!!
+
 private fun ExternalModuleDependency.intrinsicExclusions() {
     exclude(ExcludeProperties.`everything from kotlin`)
     exclude(ExcludeProperties.`everything from kotlinx`)

+ 3 - 3
buildSrc/src/main/kotlin/Shadow.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -101,7 +101,7 @@ private fun KotlinTarget.configureRelocationForMppTarget(project: Project) = pro
         destinationDirectory.set(buildDir.resolve("libs")) // build/libs
         archiveBaseName.set("${project.name}-${targetName.toLowerCase()}") // e.g. "mirai-core-api-jvm"
 
-        dependsOn(compilations["main"].compileKotlinTask) // e.g. compileKotlinJvm
+        dependsOn(compilations["main"].compileTaskProvider) // e.g. compileKotlinJvm
 
         from(compilations["main"].output) // Add compilation result of mirai sourcecode, not including dependencies
         configuration?.let {
@@ -257,7 +257,7 @@ fun Project.registerRegularShadowTask(
         }
 
         val compilation = target.compilations["main"]
-        dependsOn(compilation.compileKotlinTask)
+        dependsOn(compilation.compileTaskProvider)
         from(compilation.output)
 
 //        components.findByName("java")?.let { from(it) }

+ 41 - 0
buildSrc/src/main/kotlin/TestDependencies.kt

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+import org.gradle.api.Project
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
+
+/**
+ * 配置 test 依赖 mirai-core:jvmTest, 可访问 `AbstractTest` 等
+ * 
+ * 用法:
+ * 
+ * *build.gradle.kts*
+ * ```
+ * kotlin.sourceSets.test {
+ *     dependsOnCoreJvmTest(project)
+ * }
+ * ```
+ */
+fun KotlinSourceSet.dependsOnCoreJvmTest(project: Project) {
+    project.evaluationDependsOn(":mirai-core")
+    dependencies {
+        implementation(
+            project(":mirai-core").dependencyProject.kotlinMpp!!.targets
+                .single { it.name == "jvm" }
+                .compilations.getByName("test")
+                .output.allOutputs
+        )
+        implementation(
+            project(":mirai-core").dependencyProject.kotlinMpp!!.targets
+                .single { it.name == "jvmBase" }
+                .compilations.getByName("test")
+                .output.allOutputs
+        )
+    }
+}

+ 16 - 13
buildSrc/src/main/kotlin/Versions.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -28,18 +28,18 @@ object Versions {
 
     val core get() = project
     val console get() = project
-    val consoleIntellij get() = "223-$project-172-1" // idea-mirai-kotlin-patch
+    val consoleIntellij get() = "231-$project-182-1" // idea-mirai-kotlin-patch
     val consoleTerminal get() = project
 
-    const val kotlinCompiler = "1.7.10"
+    const val kotlinCompiler = "1.8.10"
     const val kotlinStdlib = kotlinCompiler
-    const val dokka = "1.7.10"
+    const val dokka = "1.8.10"
 
-    const val kotlinCompilerForIdeaPlugin = "1.7.20"
+    const val kotlinCompilerForIdeaPlugin = "1.8.20-RC" // 231 bundles 1.8.20
 
     const val coroutines = "1.6.4"
-    const val atomicFU = "0.18.3"
-    const val serialization = "1.3.3"
+    const val atomicFU = "0.20.0"
+    const val serialization = "1.5.0"
 
     /**
      * 注意, 不要轻易升级 ktor 版本. 阅读 [RelocationNotes], 尤其是间接依赖部分.
@@ -50,14 +50,14 @@ object Versions {
 
     const val binaryValidator = "0.4.0"
 
-    const val blockingBridge = "2.1.0-170.1"
-    const val dynamicDelegation = "0.3.0-170.1"
-    const val mavenCentralPublish = "1.0.0-dev-3"
+    const val blockingBridge = "3.0.0-180.1"
+    const val dynamicDelegation = "0.4.0-180.1"
+    const val mavenCentralPublish = "1.0.0"
 
     const val androidGradlePlugin = "4.1.1"
     const val android = "4.1.1.4"
 
-    const val shadow = "7.1.3-mirai-modified-SNAPSHOT"
+    const val shadow = "8.1.0"
 
     const val logback = "1.3.4"
     const val slf4j = "2.0.3"
@@ -72,7 +72,7 @@ object Versions {
     const val junit = "5.7.2"
 
     const val yamlkt = "0.12.0"
-    const val intellijGradlePlugin = "1.11.0"
+    const val intellijGradlePlugin = "1.13.2"
 
     // https://github.com/google/jimfs
     // Java In Memory File System
@@ -84,7 +84,7 @@ object Versions {
 
     // don't update easily unless you want your disk space -= 1000 MB
     // (700 MB for IDEA, 150 MB for sources, 150 MB for JBR)
-    const val intellij = "2022.3.1"
+    const val intellij = "2023.1"
 }
 
 @Suppress("unused")
@@ -103,6 +103,9 @@ val `kotlinx-serialization-json` = kotlinx("serialization-json", Versions.serial
 val `kotlinx-serialization-protobuf` = kotlinx("serialization-protobuf", Versions.serialization)
 const val `kotlinx-atomicfu` = "org.jetbrains.kotlinx:atomicfu:${Versions.atomicFU}"
 
+const val `kotlin-jvm-blocking-bridge` = "me.him188:kotlin-jvm-blocking-bridge-runtime:${Versions.blockingBridge}"
+const val `kotlin-dynamic-delegation` = "me.him188:kotlin-dynamic-delegation:${Versions.dynamicDelegation}"
+
 /**
  * @see relocateImplementation
  */

+ 5 - 5
ci-release-helper/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -104,19 +104,19 @@ tasks.register("updateSnapshotVersion") {
         result.assertNormalExitValue()
 
         val resultString = out.toByteArray().decodeToString()
-        val index = resultString
+        val branchAndIndex = resultString
             .substringAfter("<SNAPSHOT_VERSION_START>", "")
             .substringBefore("<SNAPSHOT_VERSION_END>", "")
 
         logger.info("Exec result:")
         logger.info(resultString)
 
-        if (index.isEmpty()) {
+        if (branchAndIndex.isEmpty()) {
             throw GradleException("Failed to find version.")
         }
 
-        logger.info("Snapshot version index is '$index'")
-        val versionName = "${Versions.project}-$branch-${index}"
+        logger.info("Snapshot version index is '$branchAndIndex'")
+        val versionName = "${Versions.project}-${branchAndIndex}"
 
         // Add annotation on GitHub Actions build
         // https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message

+ 52 - 0
ci-release-helper/changelogs/2.15.0-M1.md

@@ -0,0 +1,52 @@
+## mirai-core
+
+### 不兼容变更
+
+- 删除了旧版的为兼容 Java 生成的阻塞式方法桥
+  > 这只会导致依赖 mirai [2.1.0](https://github.com/mamoe/mirai/releases/tag/2.1.0) (发布于 2 年前) 编译的 Java 代码现在无法使用 mirai 2.15.0-M1 级以上版本运行. 将它们使用 2.15.0-M1 及以上重新编译即可运行.
+  >
+  > 这是因为 KJBB 以前有 bug, 会生成返回值为 `Unit` 的方法桥. mirai 为了兼容, 一直让 KJBB 既生成返回 `Unit` 的, 也生成返回 `void` 的. 但自 Kotiln 编译器 1.8.0 起, 其 IR lowering 会把 `companion object` 中的静态函数 `@JvmStatic` 的返回值由 `Unit` 变更为 `void`, 导致编译器插件 KJBB 不再能做兼容.
+
+### 新特性
+
+- 支持扫码登录 (#2502 with @StageGuard, #1281)
+
+新的登录方法通过 `BotAuthorization` & `BotFactory.newBot(id: Long, authorization: BotAuthorization)` 登录
+
+关于详细的使用方法请参考 `BotAuthorization` 的注释
+
+扫码登录的实现不一定稳定 (因为涉及修改了大量内部登录和维护在线逻辑), 文档也还在正在准备中.   
+**在 2.15.0-RC 可能会修改扫码登录的 API**.
+
+> mirai-console **尚未支持
+**在命令中指定扫码登录, 但是提供了 `MiraiConsole.addBot(id: Long, authorization: BotAuthorization)` 用于扫码登录
+
+### 优化和修复
+
+- 更新 Kotlin 到 1.8.10, kotlinx-serialization 到 1.5.0 (#2578)
+- 修复特殊情况可能无法加载 services 的问题 (#2268, #2511 by @Nambers, #2428 by @cssxsh)
+  > 例如在 Minecraft 插件中
+- 增加 TxCaptchaHelper 可用性无法保证的警告 (#2564 by @MrXiaoM)
+- 修正消息多态序列化, 输出的 JSON 不再包含多余的 "type" 字段 (#2414)
+- 修正群公告发送失败报错 `no login` (#2069, #2512 by @cssxsh)
+- 修正使用 `Announcements.get(fid)` 出现 `kotlinx.serialization.MissingFieldException: Field 'msg'` (#2509, #2512 by @cssxsh)
+- 修正短暂断网时不能成功重连 (#2488, #2504, #2505 by @sandtechnology)
+- 修复 `OfflineMessageSource` 回复时, 引用回复的 At 变空白的问题 (#2501)
+- 在无法连接服务器时在报错信息中携带尝试连接的服务器 (#2576 by @cssxsh)
+- 修正 dumpTlvMap 返回值不正确的问题 (内部) (#2557 by @MrXiaoM)
+- 修正文档细节 (#2547 by @7aGiven)
+
+## mirai-core-mock
+- 在 upload 后的 MockImage 中提供 size 属性 (#2515)
+
+## mirai-console
+
+### 新特性
+- JvmPlugin 以 `getResource` 方法获取全局资源文件 (#2536 by @ArgonarioD)
+- 添加新事件 `StartupEvent`, `AutoLoginEvent` (#2446 by @cssxsh)
+  > 分别在 Console 启动完成后, 和自动登录后触发
+
+### 优化和修复
+- 文档修正(#2503 by @7aGiven, #2506 by @7aGiven, #2457 by @char-46, #2577 by @cssxsh, #2491 by @EnchStudio)
+- 修复在Android系统运行时,被杀后台时抛出的 InterruptedException 导致崩溃 (#2474 by @zhaodice)
+- 修复使用 Console 扩展时,对于扩展的函数返回非 null 值报错的情况 (#2528 by @NoMathExpectation)

+ 21 - 18
ci-release-helper/src/buildIndex/SnapshotVersions.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -20,7 +20,8 @@ import kotlinx.serialization.json.Json
 object GetNextSnapshotIndex {
     @JvmStatic
     fun main(args: Array<String>) {
-        val branch = args.getOrNull(0) ?: error("Missing branch argument")
+        val branch = args.getOrNull(0)?.replace(Regex("""[/\\.,`~!@#$%^&*(){}\[\]|;]"""), "-")
+            ?: error("Missing branch argument")
         val commitRef = args.getOrNull(1) ?: error("Missing commitRef argument")
 
 
@@ -40,7 +41,7 @@ object GetNextSnapshotIndex {
                 }
                 println()
 
-                println("<SNAPSHOT_VERSION_START>${index.value}<SNAPSHOT_VERSION_END>")
+                println("<SNAPSHOT_VERSION_START>$branch-${index.value}<SNAPSHOT_VERSION_END>")
             }
         }
     }
@@ -52,13 +53,14 @@ suspend fun HttpClient.getExistingIndex(
     commitRef: String,
 ): Index? {
     // https://build.mirai.mamoe.net/v1/mirai-core/dev/indexes/?commitRef=29121565132bed6e996f3de32faaf49106ae8e39
-    val resp = get("https://build.mirai.mamoe.net/v1/$module/$branch/indexes/") {
-        basicAuth(
-            System.getenv("mirai.build.index.auth.username"),
-            System.getenv("mirai.build.index.auth.password")
-        )
-        parameter("commitRef", commitRef)
-    }
+    val resp =
+        get("https://build.mirai.mamoe.net/v1/${module.encodeURLPathPart()}/${branch.encodeURLPathPart()}/indexes/") {
+            basicAuth(
+                System.getenv("mirai.build.index.auth.username"),
+                System.getenv("mirai.build.index.auth.password")
+            )
+            parameter("commitRef", commitRef)
+        }
     if (!resp.status.isSuccess()) {
         val body = runCatching { resp.bodyAsText() }.getOrNull()
         throw IllegalStateException("Request failed: ${resp.status}  $body")
@@ -74,7 +76,7 @@ suspend fun HttpClient.createBranch(
     branch: String,
 ): Boolean {
     // https://build.mirai.mamoe.net/v1/mirai-core/dev/indexes/?commitRef=29121565132bed6e996f3de32faaf49106ae8e39
-    val resp = put("https://build.mirai.mamoe.net/v1/$module/$branch") {
+    val resp = put("https://build.mirai.mamoe.net/v1/${module.encodeURLPathPart()}/${branch.encodeURLPathPart()}") {
         basicAuth(
             System.getenv("mirai.build.index.auth.username"),
             System.getenv("mirai.build.index.auth.password")
@@ -88,13 +90,14 @@ suspend fun HttpClient.postNextIndex(
     branch: String,
     commitRef: String,
 ): Index {
-    val resp = post("https://build.mirai.mamoe.net/v1/$module/$branch/indexes/next") {
-        basicAuth(
-            System.getenv("mirai.build.index.auth.username"),
-            System.getenv("mirai.build.index.auth.password")
-        )
-        parameter("commitRef", commitRef)
-    }
+    val resp =
+        post("https://build.mirai.mamoe.net/v1/${module.encodeURLPathPart()}/${branch.encodeURLPathPart()}/indexes/next") {
+            basicAuth(
+                System.getenv("mirai.build.index.auth.username"),
+                System.getenv("mirai.build.index.auth.password")
+            )
+            parameter("commitRef", commitRef)
+        }
     if (!resp.status.isSuccess()) {
         val body = runCatching { resp.bodyAsText() }.getOrNull()
         throw IllegalStateException("Request failed: ${resp.status}  $body")

+ 5 - 5
docs/ConsoleTerminal.md

@@ -202,11 +202,11 @@ Console 会自动根据语境推断指令参数的含义。
 
 #### 群
 
-| 格式        | 示例              | 说明                   |
-|-----------|-----------------|----------------------|
-| 机器人号码.群号码 | `123456.987654` | 一个机器人的一个群            |
-| 群号码       | `987654`        | 当前唯一在线机器人的一个群        |
-| `~`       | `~`             | 仅聊天环境下,指代指令调用人自己作为好友 |
+| 格式        | 示例              | 说明            |
+|-----------|-----------------|---------------|
+| 机器人号码.群号码 | `123456.987654` | 一个机器人的一个群     |
+| 群号码       | `987654`        | 当前唯一在线机器人的一个群 |
+| `~`       | `~`             | 仅聊天环境下,指代当前群聊 |
 
 配置
 ----

+ 2 - 0
docs/README.md

@@ -100,6 +100,7 @@ HTTP 插件),也可以阅读 [用户手册](UserManual.md) 进行个性化
 
 [MR-XieXuan/MiraiTravel]:https://github.com/MR-XieXuan/MiraiTravel
 
+[yuansicloud/Abp.Mirai]:https://github.com/yuansicloud/Abp.Mirai
 ### 原生接口
 
 这些接口直接在 JVM 上实现,不需要中间件,拥有更佳的性能。
@@ -134,6 +135,7 @@ HTTP 插件),也可以阅读 [用户手册](UserManual.md) 进行个性化
 | `C#`                      | [AhpxChina/Mirai.Net]                |
 | `C#`                      | [Cyl18/Chaldene]                     |
 | `C#`                      | [Miyakowww/CocoaFramework2]          |
+| `C#`                      | [yuansicloud/Abp.Mirai]              |
 | `C++`                     | [cyanray/mirai-cpp]                  |
 | `C++`                     | [Chlorie/miraipp]                    |
 | `GDScript`                | [Xwdit/RainyBot-Core]                |

+ 29 - 5
docs/UserManual.md

@@ -1,12 +1,14 @@
 # Mirai - UserManual
 
-Mirai 用户手册。本文面向对开发不熟悉而希望使用 Mirai 的用户。如果你要开发,请先阅读 [开发文档](README.md)。
+这里是 Mirai 用户手册。本文面向对开发并不熟悉,但希望使用 Mirai 提供的 QQ 机器人服务支持的用户。
+如果你要开发 Mirai 插件或参与贡献 Mirai 项目,请先阅读 [开发文档](README.md)。  
 
 ## 启动 Mirai
 
-使用 Mirai,一般人要启动的是 Mirai 控制台(即 Mirai Console),它可以加载插件。
+想要部署并使用 Mirai QQ机器人框架,只需要启动 Mirai 控制台(即 Mirai Console),
+它自带一些基础功能,也可以加载社区提供的插件。
 
-Mirai 控制台现在有两个版本,插件在这两个版本的 Mirai Console 上都可以运行:
+Mirai 控制台现在有两个版本,Mirai 插件在这两个版本的 Mirai Console 上都可以运行:
 
 [MCLI-1.png]: .UserManual_images/MCLI-1.png
 
@@ -20,11 +22,33 @@ Mirai 控制台现在有两个版本,插件在这两个版本的 Mirai Console
 ## 使用图形界面版本
 
 前往 [sonder-joker/mirai-compose](https://github.com/sonder-joker/mirai-compose/releases)
-下载适合你的系统的压缩包,解压到一个文件就可以使用。
+下载适合你的系统的压缩包,  
+>  MAC 系统下载 .dmg 后缀的文件  
+>  Windows 系统下载 .msi 后缀的文件  
+>  Linux 系统下载 .deb 后缀的文件  
+
+以 Windows 系统为例,以下为简要安装步骤:  
+
+1. 下载 `mirai-compose-<版本>.msi`
+2. 双击运行安装程序,选择一个合适的文件夹,然后点击安装
+3. 安装完毕后打开刚才指定的文件夹
+4. 双击启动其中的 `mirai-compose.exe` 即可开始运行
+5. 运行后点击左上角可以添加 QQ bot 账号
+
+安装插件只需要将下载好的插件置于 plugins 目录,安装完毕后重启 mirai-compose 以生效。
 
 ## 使用纯控制台版本
 
-查看 [ConsoleTerminal.md](ConsoleTerminal.md)。
+详细教程请查看 [ConsoleTerminal.md](ConsoleTerminal.md)。
+
+以 Windows 系统为例,以下为简要安装步骤:  
+
+1. 前往 [iTXTech/mcl-installer](https://github.com/iTXTech/mcl-installer/releases) 下载适合您系统的最新版本的 MCL 安装器
+2. 创建好文件夹之后,将 MCL 安装器移动到其中
+3. 双击 `mcl-installer.exe` 过程中只需要按几次回车键,即可安装完毕
+4. 运行 `mcl.cmd` 即可启动 MCL 控制台
+
+安装插件只需要将下载好的插件置于 plugins 目录,然后重启 MCL 控制台即可。  
 
 ## 解决问题
 

+ 41 - 2
mirai-console/backend/integration-test/testers/service-loader/service-loader-2dep-plugin/src/PMain.kt

@@ -17,6 +17,7 @@ import java.util.*
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertNull
+import kotlin.test.assertTrue
 
 
 internal class PS : ServiceTypedef
@@ -64,7 +65,45 @@ internal object PMain : KotlinPlugin(JvmPluginDescription("net.mamoe.console.ite
                     println(it)
                 }.size
         )
-        assertNull(javaClass.getResource("/net/mamoe/mirai/console/MiraiConsole.class"))
-        assertNull(javaClass.getResource("/net/mamoe/mirai/Bot.class"))
+
+        // ************************* resources loading tests *************************
+
+        val miraiConsoleClassPath = "net/mamoe/mirai/console/MiraiConsole.class"
+        val miraiBotClassPath = "net/mamoe/mirai/Bot.class"
+        val allClassesPath = "META-INF/mirai-console/allclasses.txt"
+
+        fun ClassLoader.getSizeOfResources(path: String): Int {
+            return this.getResources(path).toList().size
+        }
+
+        assertNull(javaClass.getResource("/$miraiConsoleClassPath"))
+        assertNull(javaClass.classLoader.getResource(miraiConsoleClassPath))
+        val miraiConsoleClassResourceCount = javaClass.classLoader.getSizeOfResources(miraiConsoleClassPath)
+            .also { assertEquals(0, it) }
+
+        assertNull(javaClass.getResource("/$miraiBotClassPath"))
+        assertNull(javaClass.classLoader.getResource(miraiBotClassPath))
+
+        val allClassesResourceCount = javaClass.classLoader.getSizeOfResources(allClassesPath)
+            .also { assertEquals(0, it) }
+
+        jvmPluginClasspath.shouldResolveConsoleSystemResource = true
+
+        assertNotNull(javaClass.classLoader.getResource(miraiConsoleClassPath))
+        assertNotNull(javaClass.classLoader.getResource(miraiBotClassPath))
+
+        assertTrue(javaClass.classLoader.getSizeOfResources(miraiConsoleClassPath) > miraiConsoleClassResourceCount)
+        assertTrue(javaClass.classLoader.getSizeOfResources(allClassesPath) > allClassesResourceCount)
+
+        jvmPluginClasspath.shouldResolveConsoleSystemResource = false
+
+        assertNull(javaClass.getResource("/$miraiConsoleClassPath"))
+        assertNull(javaClass.classLoader.getResource(miraiConsoleClassPath))
+        assertEquals(0, javaClass.classLoader.getSizeOfResources(miraiConsoleClassPath))
+
+        assertNull(javaClass.getResource("/$miraiBotClassPath"))
+        assertNull(javaClass.classLoader.getResource(miraiBotClassPath))
+
+        assertEquals(0, javaClass.classLoader.getSizeOfResources(allClassesPath))
     }
 }

+ 3 - 1
mirai-console/backend/mirai-console/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -62,6 +62,8 @@ dependencies {
     smartImplementation(`maven-resolver-connector-basic`)
     smartImplementation(`maven-resolver-transport-http`)
     smartImplementation(`slf4j-api`)
+    smartImplementation(`kotlin-jvm-blocking-bridge`)
+    smartImplementation(`kotlin-dynamic-delegation`)
     smartApi(`kotlinx-coroutines-jdk8`)
 
     testApi(project(":mirai-core"))

+ 4 - 2
mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api

@@ -19,6 +19,7 @@ public abstract interface class net/mamoe/mirai/console/MiraiConsole : kotlinx/c
 
 public final class net/mamoe/mirai/console/MiraiConsole$INSTANCE : net/mamoe/mirai/console/MiraiConsole {
 	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLjava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
+	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;JLnet/mamoe/mirai/auth/BotAuthorization;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
 	public static synthetic fun addBot$default (Lnet/mamoe/mirai/console/MiraiConsole$INSTANCE;J[BLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/Bot;
 	public fun getBuildDate ()Ljava/time/Instant;
 	public fun getBuiltInPluginLoaders ()Ljava/util/List;
@@ -1107,6 +1108,7 @@ public abstract interface class net/mamoe/mirai/console/data/PluginConfig : net/
 
 public abstract interface class net/mamoe/mirai/console/data/PluginData {
 	public abstract fun getSaveName ()Ljava/lang/String;
+	public fun getSaveType ()Lnet/mamoe/mirai/console/data/PluginData$SaveType;
 	public abstract fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
 	public abstract fun getUpdaterSerializer ()Lkotlinx/serialization/KSerializer;
 }
@@ -1859,7 +1861,6 @@ public final class net/mamoe/mirai/console/plugin/ResourceContainer$Companion {
 
 public final class net/mamoe/mirai/console/plugin/center/PluginCenter$PluginInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/console/plugin/center/PluginCenter$PluginInfo$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/console/plugin/center/PluginCenter$PluginInfo;
@@ -1875,7 +1876,6 @@ public final class net/mamoe/mirai/console/plugin/center/PluginCenter$PluginInfo
 
 public final class net/mamoe/mirai/console/plugin/center/PluginCenter$PluginInsight$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/console/plugin/center/PluginCenter$PluginInsight$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/console/plugin/center/PluginCenter$PluginInsight;
@@ -2023,6 +2023,8 @@ public abstract interface class net/mamoe/mirai/console/plugin/jvm/JvmPluginClas
 	public abstract fun getPluginFile ()Ljava/io/File;
 	public abstract fun getPluginIndependentLibrariesClassLoader ()Ljava/lang/ClassLoader;
 	public abstract fun getPluginSharedLibrariesClassLoader ()Ljava/lang/ClassLoader;
+	public abstract fun getShouldResolveConsoleSystemResource ()Z
+	public abstract fun setShouldResolveConsoleSystemResource (Z)V
 }
 
 public abstract interface class net/mamoe/mirai/console/plugin/jvm/JvmPluginDescription : net/mamoe/mirai/console/plugin/description/PluginDescription {

+ 30 - 5
mirai-console/backend/mirai-console/src/MiraiConsole.kt

@@ -16,6 +16,7 @@ import kotlinx.coroutines.*
 import me.him188.kotlin.dynamic.delegation.dynamicDelegation
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.console.MiraiConsole.INSTANCE
 import net.mamoe.mirai.console.MiraiConsoleImplementation.Companion.start
 import net.mamoe.mirai.console.extensions.BotConfigurationAlterer
@@ -191,8 +192,31 @@ public interface MiraiConsole : CoroutineScope {
         public fun addBot(id: Long, password: ByteArray, configuration: BotConfiguration.() -> Unit = {}): Bot =
             addBotImpl(id, password, configuration)
 
+        /**
+         * 添加一个 [Bot] 实例到全局 Bot 列表, 但不登录.
+         *
+         * 调用 [Bot.login] 可登录.
+         *
+         * @see Bot.instances 获取现有 [Bot] 实例列表
+         * @see BotConfigurationAlterer ExtensionPoint
+         */
+        @ConsoleExperimentalApi("This is a low-level API and might be removed in the future.")
+        public fun addBot(
+            id: Long,
+            authorization: BotAuthorization,
+            configuration: BotConfiguration.() -> Unit = {}
+        ): Bot = addBotImpl(id, authorization, configuration)
+
         @Suppress("UNREACHABLE_CODE")
-        private fun addBotImpl(id: Long, password: Any, configuration: BotConfiguration.() -> Unit = {}): Bot {
+        private fun addBotImpl(id: Long, authorization: Any, configuration: BotConfiguration.() -> Unit = {}): Bot {
+            when (authorization) {
+                is String -> {}
+                is ByteArray -> {}
+                is BotAuthorization -> {}
+
+                else -> throw IllegalArgumentException("Bad authorization type: `${authorization.javaClass.name}`. Require String, ByteArray or BotAuthorization")
+            }
+
             var config = BotConfiguration().apply {
 
                 workingDir = MiraiConsole.rootDir
@@ -239,10 +263,11 @@ public interface MiraiConsole : CoroutineScope {
                 extension.alterConfiguration(id, acc)
             }
 
-            return when (password) {
-                is ByteArray -> BotFactory.newBot(id, password, config)
-                is String -> BotFactory.newBot(id, password, config)
-                else -> throw IllegalArgumentException("Bad password type: `${password.javaClass.name}`. Require ByteArray or String")
+            return when (authorization) {
+                is ByteArray -> BotFactory.newBot(id, authorization, config)            // pwd md5
+                is String -> BotFactory.newBot(id, authorization, config)               // pwd
+                is BotAuthorization -> BotFactory.newBot(id, authorization, config)     // authorization
+                else -> error("assert")
             }
         }
 

+ 2 - 2
mirai-console/backend/mirai-console/src/data/AbstractPluginData.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -78,7 +78,7 @@ public abstract class AbstractPluginData : PluginData, PluginDataImpl() {
     public final override val updaterSerializer: KSerializer<Unit>
         get() = super.updaterSerializer
 
-    public override val serializersModule: SerializersModule get() = EmptySerializersModule
+    public override val serializersModule: SerializersModule get() = EmptySerializersModule()
     /**
      * 当所属于这个 [PluginData] 的 [Value] 的 [值][Value.value] 被修改时被调用.
      */

+ 15 - 0
mirai-console/backend/mirai-console/src/data/PluginData.kt

@@ -122,6 +122,21 @@ public interface PluginData {
     @ConsoleExperimentalApi
     public val saveName: String
 
+    /**
+     * [PluginData] 序列化时使用的格式的枚举.
+     */
+    @ConsoleExperimentalApi
+    public enum class SaveType(@ConsoleExperimentalApi public val extension: String) {
+        YAML("yml"), JSON("json")
+    }
+
+    /**
+     * 决定这个 [PluginData] 序列化时使用的格式, 默认为 YAML.
+     * 具体实现格式由 [PluginDataStorage] 决定.
+     */
+    @ConsoleExperimentalApi
+    public val saveType: SaveType get() = SaveType.YAML
+
     @ConsoleExperimentalApi
     public val updaterSerializer: KSerializer<Unit>
 

+ 8 - 0
mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt

@@ -33,6 +33,7 @@ import net.mamoe.mirai.console.extensions.CommandCallParserProvider
 import net.mamoe.mirai.console.extensions.CommandCallResolverProvider
 import net.mamoe.mirai.console.extensions.PermissionServiceProvider
 import net.mamoe.mirai.console.extensions.PostStartupExtension
+import net.mamoe.mirai.console.internal.auth.ConsoleSecretsCalculator
 import net.mamoe.mirai.console.internal.command.CommandConfig
 import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig
 import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.ConfigurationKey
@@ -100,6 +101,9 @@ internal class MiraiConsoleImplementationBridge(
     @Volatile
     var permissionSeviceLoaded: Boolean = false
 
+    // For protect account.secrets in console with non-password login
+    lateinit var consoleSecretsCalculator: ConsoleSecretsCalculator
+
     // MiraiConsoleImplementation define: get() = LoggerControllerImpl()
     // Need to cache it or else created every call.
     //      It caused config/Console/Logger.yml ignored.
@@ -290,6 +294,10 @@ ___  ____           _   _____                       _
         phase("initialize all plugins") {
             pluginManager // init
 
+            consoleSecretsCalculator = ConsoleSecretsCalculator(
+                pluginManager.pluginsDataPath.resolve("Console/console-secrets.key")
+            ).also { it.consoleKey }
+
             mainLogger.verbose { "Loading JVM plugins..." }
             pluginManager.loadAllPluginsUsingBuiltInLoaders()
             pluginManager.initExternalPluginLoaders().let { count ->

+ 53 - 0
mirai-console/backend/mirai-console/src/internal/auth/ConsoleBotAuthorization.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.auth
+
+import net.mamoe.mirai.auth.BotAuthInfo
+import net.mamoe.mirai.auth.BotAuthResult
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.console.MiraiConsoleImplementation
+import java.io.ByteArrayOutputStream
+
+internal class ConsoleBotAuthorization(
+    private val delegate: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult,
+) : BotAuthorization {
+
+    override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult {
+        return delegate.invoke(session, info)
+    }
+
+    override fun calculateSecretsKey(bot: BotAuthInfo): ByteArray {
+        val calc = MiraiConsoleImplementation.getBridge().consoleSecretsCalculator
+
+        val writer = ByteArrayOutputStream()
+
+        writer += calc.consoleKey.asByteArray
+
+        writer += bot.deviceInfo.apn
+        writer += bot.deviceInfo.device
+        writer += bot.deviceInfo.bootId
+        writer += bot.deviceInfo.imsiMd5
+
+        return writer.toByteArray()
+    }
+
+
+    private operator fun ByteArrayOutputStream.plusAssign(data: ByteArray) {
+        write(data)
+    }
+
+    companion object {
+        fun byQRCode(): ConsoleBotAuthorization = ConsoleBotAuthorization { session, _ ->
+            session.authByQRCode()
+        }
+    }
+}
+

+ 53 - 0
mirai-console/backend/mirai-console/src/internal/auth/ConsoleSecretsCalculator.kt

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.internal.auth
+
+import net.mamoe.mirai.utils.SecretsProtection
+import net.mamoe.mirai.utils.lateinitMutableProperty
+import java.io.ByteArrayOutputStream
+import java.io.DataOutputStream
+import java.nio.file.Path
+import java.util.*
+import kotlin.io.path.createDirectories
+import kotlin.io.path.isRegularFile
+import kotlin.io.path.readBytes
+import kotlin.io.path.writeBytes
+
+internal class ConsoleSecretsCalculator(
+    private val file: Path,
+) {
+    internal val consoleKey: SecretsProtection.EscapedByteBuffer get() = _consoleKey
+
+    private var _consoleKey: SecretsProtection.EscapedByteBuffer by lateinitMutableProperty {
+        loadOrCreate()
+    }
+
+    fun loadOrCreate(): SecretsProtection.EscapedByteBuffer {
+        if (file.isRegularFile()) {
+            return SecretsProtection.EscapedByteBuffer(file.readBytes())
+        }
+
+        file.parent?.createDirectories()
+        val dataStream = ByteArrayOutputStream()
+        val dataWriter = DataOutputStream(dataStream)
+
+        repeat(3) {
+            dataWriter.writeUTF(UUID.randomUUID().toString())
+        }
+
+        val data = dataStream.toByteArray()
+        file.writeBytes(data)
+        return SecretsProtection.EscapedByteBuffer(data)
+    }
+
+    fun reloadOrCreate() {
+        _consoleKey = loadOrCreate()
+    }
+}

+ 55 - 17
mirai-console/backend/mirai-console/src/internal/data/MultiFilePluginDataStorageImpl.kt

@@ -40,9 +40,18 @@ internal open class MultiFilePluginDataStorageImpl(
         val file = getPluginDataFile(holder, instance)
         val text = file.readText().removePrefix("\uFEFF")
         if (text.isNotBlank()) {
-            val yaml = createYaml(instance)
             try {
-                yaml.decodeFromString(instance.updaterSerializer, text)
+                when (instance.saveType) {
+                    PluginData.SaveType.YAML -> {
+                        val yaml = createYaml(instance)
+                        yaml.decodeFromString(instance.updaterSerializer, text)
+                    }
+
+                    PluginData.SaveType.JSON -> {
+                        val json = createJson(instance)
+                        json.decodeFromString(instance.updaterSerializer, text)
+                    }
+                }
             } catch (cause: Throwable) {
                 // backup data file
                 file.copyTo(file.resolveSibling("${file.name}.${currentTimeMillis()}.bak"))
@@ -67,7 +76,7 @@ internal open class MultiFilePluginDataStorageImpl(
         }
         dir.mkdir()
 
-        val file = dir.resolve("$name.yml")
+        val file = dir.resolve("$name.${instance.saveType.extension}")
         if (file.isDirectory) {
             error("Target File $file is occupied by a directory therefore data ${instance::class.qualifiedNameOrTip} can't be saved.")
         }
@@ -82,27 +91,41 @@ internal open class MultiFilePluginDataStorageImpl(
     public override fun store(holder: PluginDataHolder, instance: PluginData) {
         getPluginDataFile(holder, instance).writeText(
             kotlin.runCatching {
-                createYaml(instance).encodeToString(instance.updaterSerializer, Unit).also {
-                    Yaml.decodeAnyFromString(it) // test yaml
+                when (instance.saveType) {
+                    PluginData.SaveType.YAML -> {
+                        val yaml = createYaml(instance)
+                        yaml.encodeToString(instance.updaterSerializer, Unit).also {
+                            yaml.decodeAnyFromString(it) // test yaml
+                        }
+                    }
+
+                    PluginData.SaveType.JSON -> {
+                        val json = createJson(instance)
+                        json.encodeToString(instance.updaterSerializer, Unit).also {
+                            json.decodeFromString(instance.updaterSerializer, it) // test json
+                        }
+                    }
                 }
             }.recoverCatching {
                 logger.warning(
-                    "Could not save ${instance.saveName} in YAML format due to exception in YAML encoder. " +
+                    "Could not save ${instance.saveName} in ${instance.saveType.name} format due to exception in ${instance.saveType.name} encoder. " +
                             "Please report this exception and relevant configurations to https://github.com/mamoe/mirai/issues/new/choose",
                     it
                 )
-                @Suppress("JSON_FORMAT_REDUNDANT")
-                Json {
-                    serializersModule = MessageSerializers.serializersModule + instance.serializersModule
-
-                    prettyPrint = true
-                    ignoreUnknownKeys = true
-                    isLenient = true
-                    allowStructuredMapKeys = true
-                    encodeDefaults = true
-                }.encodeToString(instance.updaterSerializer, Unit)
+
+                if (instance.saveType == PluginData.SaveType.JSON) {
+                    throw it
+                }
+
+                val json = createJson(instance)
+                json.encodeToString(instance.updaterSerializer, Unit).also { string ->
+                    json.decodeFromString(instance.updaterSerializer, string) // test json
+                }
             }.getOrElse {
-                throw IllegalStateException("Exception while saving $instance, saveName=${instance.saveName}", it)
+                throw IllegalStateException(
+                    "Exception while saving $instance, saveName=${instance.saveName} in json format",
+                    it
+                )
             }
         )
 //        logger.verbose { "Successfully saved PluginData: ${instance.saveName} (containing ${instance.castOrNull<AbstractPluginData>()?.valueNodes?.size} properties)" }
@@ -114,6 +137,21 @@ internal open class MultiFilePluginDataStorageImpl(
                 MessageSerializers.serializersModule + instance.serializersModule // MessageSerializers.serializersModule is dynamic
         }
     }
+
+    private fun createJson(instance: PluginData): Json {
+        return Json {
+            serializersModule =
+                MessageSerializers.serializersModule + instance.serializersModule // MessageSerializers.serializersModule is dynamic
+
+            prettyPrint = true
+            ignoreUnknownKeys = true
+            isLenient = true
+            allowStructuredMapKeys = true
+            encodeDefaults = true
+
+            classDiscriminator = "#class"
+        }
+    }
 }
 
 internal fun Path.mkdir(): Boolean = this.toFile().mkdir()

+ 22 - 13
mirai-console/backend/mirai-console/src/internal/data/collectionUtil.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
+ * 此源代码的使用受 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
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 @file:Suppress("DuplicatedCode")
@@ -128,15 +128,19 @@ internal open class ShadowMap<K, V, KR, VR>(
             mappingFunction.apply(k.let(kTransform)).let(vTransformBack)
         }.let(vTransform)
 
+    @Suppress("WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE")
     override fun computeIfPresent(key: KR, remappingFunction: BiFunction<in KR, in VR, out VR?>): VR? =
         originMapComputer().computeIfPresent(key.let(kTransformBack)) { k, v ->
             remappingFunction.apply(k.let(kTransform), v.let(vTransform))?.let(vTransformBack)
         }?.let(vTransform)
 
-    override fun merge(key: KR, value: VR, remappingFunction: BiFunction<in VR, in VR, out VR?>): VR? =
-        originMapComputer().merge(key.let(kTransformBack), value.let(vTransformBack)) { k, v ->
+    @Suppress("WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE")
+    override fun merge(key: KR, value: VR, remappingFunction: BiFunction<in VR, in VR, out VR?>): VR? {
+        @Suppress("NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER")
+        return originMapComputer().merge(key.let(kTransformBack), value.let(vTransformBack)) { k, v ->
             remappingFunction.apply(k.let(vTransform), v.let(vTransform))?.let(vTransformBack)
         }?.let(vTransform)
+    }
 
     override fun forEach(action: BiConsumer<in KR, in VR>) {
         @Suppress("JavaMapForEach")
@@ -186,7 +190,9 @@ internal inline fun <E, R> MutableCollection<E>.shadowMap(
 
         override fun remove(element: R): Boolean = [email protected] { it.let(transform) == element }
         override fun removeAll(elements: Collection<R>): Boolean = elements.all(::remove)
-        override fun retainAll(elements: Collection<R>): Boolean = [email protected](elements.map(transformBack))
+        override fun retainAll(elements: Collection<R>): Boolean =
+            [email protected](elements.mapTo(HashSet(elements.size), transformBack))
+
         override fun toString(): String = [email protected]()
         override fun hashCode(): Int = [email protected]()
     }
@@ -293,7 +299,9 @@ internal inline fun <E, R> MutableSet<E>.shadowMap(
 
         override fun remove(element: R): Boolean = [email protected] { it.let(transform) == element }
         override fun removeAll(elements: Collection<R>): Boolean = elements.all(::remove)
-        override fun retainAll(elements: Collection<R>): Boolean = [email protected](elements.map(transformBack))
+        override fun retainAll(elements: Collection<R>): Boolean =
+            [email protected](elements.mapTo(HashSet(elements.size), transformBack))
+
         override fun toString(): String = [email protected]()
         override fun hashCode(): Int = [email protected]()
     }
@@ -384,7 +392,8 @@ internal inline fun <T> dynamicMutableSet(crossinline supplier: () -> MutableSet
     "ACCIDENTAL_OVERRIDE", "TYPE_MISMATCH", "NOTHING_TO_OVERRIDE",
     "MANY_IMPL_MEMBER_NOT_IMPLEMENTED", "MANY_INTERFACES_MEMBER_NOT_IMPLEMENTED",
     "UNCHECKED_CAST", "USELESS_CAST", "ACCIDENTAL_OVERRIDE",
-    "EXPLICIT_OVERRIDE_REQUIRED_IN_MIXED_MODE", "CONFLICTING_INHERITED_JVM_DECLARATIONS"
+    "EXPLICIT_OVERRIDE_REQUIRED_IN_MIXED_MODE", "CONFLICTING_INHERITED_JVM_DECLARATIONS",
+    "WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE", "NULLABLE_TYPE_PARAMETER_AGAINST_NOT_NULL_TYPE_PARAMETER"
 ) // type inference bug
 internal fun <K, V> MutableMap<K, V>.observable(onChanged: () -> Unit): MutableMap<K, V> {
     open class ObservableMap : MutableMap<K, V> by (this as MutableMap<K, V>) {
@@ -527,10 +536,10 @@ internal inline fun <T> MutableCollection<T>.observable(crossinline onChanged: (
         override fun clear() = [email protected]().also { onChanged() }
         override fun remove(element: T): Boolean = [email protected](element).also { onChanged() }
         override fun removeAll(elements: Collection<T>): Boolean =
-            [email protected](elements).also { onChanged() }
+            [email protected](elements.toSet()).also { onChanged() }
 
         override fun retainAll(elements: Collection<T>): Boolean =
-            [email protected](elements).also { onChanged() }
+            [email protected](elements.toSet()).also { onChanged() }
 
         override fun toString(): String = [email protected]()
         override fun hashCode(): Int = [email protected]()
@@ -557,10 +566,10 @@ internal inline fun <T> MutableSet<T>.observable(crossinline onChanged: () -> Un
         override fun clear() = [email protected]().also { onChanged() }
         override fun remove(element: T): Boolean = [email protected](element).also { onChanged() }
         override fun removeAll(elements: Collection<T>): Boolean =
-            [email protected](elements).also { onChanged() }
+            [email protected](elements.toSet()).also { onChanged() }
 
         override fun retainAll(elements: Collection<T>): Boolean =
-            [email protected](elements).also { onChanged() }
+            [email protected](elements.toSet()).also { onChanged() }
 
         override fun toString(): String = [email protected]()
         override fun hashCode(): Int = [email protected]()

+ 37 - 6
mirai-console/backend/mirai-console/src/internal/plugin/JvmPluginClassLoader.kt

@@ -22,6 +22,7 @@ import java.net.URLClassLoader
 import java.util.*
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.zip.ZipFile
+import kotlin.collections.LinkedHashSet
 
 /*
 Class resolving:
@@ -146,6 +147,29 @@ internal class DynLibClassLoader : DynamicClasspathClassLoader {
             }
             return null
         }
+
+        fun tryFastOrStrictResolveResources(name: String): Enumeration<URL> {
+            if (name.startsWith("java/")) return JavaSystemPlatformClassLoader.getResources(name)
+
+            // All mirai-core hard-linked should use same version to avoid errors (ClassCastException).
+            val fromDependencies = AllDependenciesClassesHolder.appClassLoader.getResources(name)
+
+            return if (
+                name.startsWith("net/mamoe/mirai/")
+                || name.startsWith("kotlin/")
+                || name.startsWith("kotlinx/")
+                || name.startsWith("org/slf4j/")
+            ) { // Avoid plugin classing cheating
+                fromDependencies
+            } else {
+                LinkedHashSet<URL>().apply {
+                    addAll(fromDependencies)
+                    addAll(JavaSystemPlatformClassLoader.getResources(name))
+                }.let {
+                    Collections.enumeration(it)
+                }
+            }
+        }
     }
 
     internal fun loadClassInThisClassLoader(name: String): Class<*>? {
@@ -458,13 +482,12 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
         }
         src.add(pluginIndependentCL.getResources(name))
 
-        val resolved = mutableListOf<URL>()
-        src.forEach { nested ->
-            nested.iterator().forEach { url ->
-                if (url !in resolved)
-                    resolved.add(url)
-            }
+        val resolved = LinkedHashSet<URL>()
+
+        if (openaccess.shouldResolveConsoleSystemResource) {
+            DynLibClassLoader.tryFastOrStrictResolveResources(name).let { resolved.addAll(it) }
         }
+        src.forEach { nested -> resolved.addAll(nested) }
 
         return Collections.enumeration(resolved)
     }
@@ -489,6 +512,12 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
         if (name.startsWith("META-INF/services/net.mamoe.mirai.console.plugin."))
             return findResource(name)
 
+        if (openaccess.shouldResolveConsoleSystemResource) {
+            DynLibClassLoader.tryFastOrStrictResolveResources(name)
+                .takeIf { it.hasMoreElements() }
+                ?.let { return it.nextElement() }
+        }
+
         findResource(name)?.let { return it }
         // parent: ctx.sharedLibrariesLoader
         sharedLibrariesLogger.getResource(name)?.let { return it }
@@ -514,6 +543,8 @@ internal class JvmPluginClassLoaderN : URLClassLoader {
         override val pluginIndependentLibrariesClassLoader: ClassLoader
             get() = pluginIndependentCL
 
+        override var shouldResolveConsoleSystemResource: Boolean = false
+
         private val permitted by lazy {
             arrayOf(
                 this@JvmPluginClassLoaderN,

+ 7 - 0
mirai-console/backend/mirai-console/src/plugin/jvm/JvmPluginClasspath.kt

@@ -41,6 +41,13 @@ public interface JvmPluginClasspath {
      */
     public val pluginIndependentLibrariesClassLoader: ClassLoader
 
+    /**
+     * [pluginClassLoader] 是否可以通过 [ClassLoader.getResource] 获取 Mirai Console (包括依赖) 的相关资源
+     *
+     * 默认为 `false`
+     */
+    public var shouldResolveConsoleSystemResource: Boolean
+
     /**
      * 将 [file] 加入 [classLoader] 的搜索路径内
      *

+ 0 - 157
mirai-console/backend/mirai-console/test/command/LoginCommandTest.kt

@@ -1,157 +0,0 @@
-/*
- * Copyright 2019-2022 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/dev/LICENSE
- */
-
-@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-
-package net.mamoe.mirai.console.command
-
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.test.runTest
-import net.mamoe.mirai.Bot
-import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
-import net.mamoe.mirai.console.command.descriptor.ExperimentalCommandDescriptors
-import net.mamoe.mirai.console.internal.command.builtin.LoginCommandImpl
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account
-import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.PasswordKind
-import net.mamoe.mirai.internal.QQAndroidBot
-import net.mamoe.mirai.utils.BotConfiguration
-import net.mamoe.mirai.utils.md5
-import net.mamoe.mirai.utils.toUHexString
-import kotlin.test.Test
-import kotlin.test.assertContentEquals
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-
-@OptIn(ExperimentalCommandDescriptors::class)
-internal class LoginCommandTest : AbstractCommandTest() {
-
-    @Test
-    fun `login with provided password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        val bot = awaitDeferred { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId $myPwd")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved plain password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.PLAIN, myPwd)
-                )
-            )
-        })
-
-        val bot = awaitDeferred { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved md5 password`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString(""))
-                )
-            )
-        })
-
-        val bot = awaitDeferred<QQAndroidBot> { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val account = bot.account
-        assertContentEquals(myPwd.md5(), account.passwordMd5)
-        assertEquals(myId, account.id)
-    }
-
-    @Test
-    fun `login with saved configuration`() = runTest {
-        val myId = 123L
-        val myPwd = "password001"
-
-        dataScope.set(AutoLoginConfig().apply {
-            accounts.add(
-                Account(
-                    account = myId.toString(),
-                    password = Account.Password(PasswordKind.MD5, myPwd.md5().toUHexString("")),
-                    configuration = mapOf(
-                        Account.ConfigurationKey.protocol to BotConfiguration.MiraiProtocol.ANDROID_PAD.name,
-                        Account.ConfigurationKey.device to "device.new.json",
-                        Account.ConfigurationKey.heartbeatStrategy to BotConfiguration.HeartbeatStrategy.REGISTER.name
-                    )
-                )
-            )
-        })
-
-        val bot = awaitDeferred<QQAndroidBot> { cont ->
-            val command = object : LoginCommandImpl() {
-                override suspend fun doLogin(bot: Bot) {
-                    cont.complete(bot as QQAndroidBot)
-                }
-            }
-            command.register(true)
-            command.execute(consoleSender, "$myId")
-        }
-
-        val configuration = bot.configuration
-        assertEquals(BotConfiguration.MiraiProtocol.ANDROID_PAD, configuration.protocol)
-        assertEquals(BotConfiguration.HeartbeatStrategy.REGISTER, configuration.heartbeatStrategy)
-        assertNotNull(configuration.deviceInfo)
-    }
-}
-
-@BuilderInference
-internal suspend inline fun <T> awaitDeferred(
-    @BuilderInference
-    crossinline block: suspend (CompletableDeferred<T>) -> Unit
-): T {
-    val deferred = CompletableDeferred<T>()
-    block(deferred)
-    return deferred.await()
-}

+ 159 - 0
mirai-console/backend/mirai-console/test/data/MultiFilePluginDataStorageImplTests.kt

@@ -0,0 +1,159 @@
+/*
+ * Copyright 2019-2022 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.console.data
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonClassDiscriminator
+import net.mamoe.mirai.console.internal.data.MultiFilePluginDataStorageImpl
+import net.mamoe.mirai.console.testFramework.AbstractConsoleInstanceTest
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.io.TempDir
+import java.nio.file.Path
+import kotlin.test.assertEquals
+
+internal class MultiFilePluginDataStorageImplTests : AbstractConsoleInstanceTest() {
+    @TempDir
+    internal lateinit var storePath: Path
+
+    @Serializable
+    @JsonClassDiscriminator("base_type")
+    internal sealed class Base // not using interface, see https://github.com/Kotlin/kotlinx.serialization/issues/2181
+
+    @Serializable
+    @SerialName("DerivedA")
+    internal data class DerivedA(val valueA: Double) : Base()
+
+    @Serializable
+    @SerialName("DerivedB")
+    internal data class DerivedB(val valueB: String) : Base()
+
+    @Serializable
+    @SerialName("DerivedC")
+    internal object DerivedC : Base() {
+        @Suppress("unused")
+        const val valueC: Int = 42
+    }
+
+    private class YamlPluginData : AutoSavePluginData("test_yaml") {
+        var int by value(1)
+        val map: MutableMap<String, String> by value()
+        val map2: MutableMap<String, MutableMap<String, String>> by value()
+
+        companion object {
+            val string = """
+                int: 2
+                map: 
+                  key1: value1
+                  key2: value2
+                map2: 
+                  key1: 
+                    key1: value1
+                    key2: value2
+                  key2: 
+                    key1: value1
+                    key2: value2
+            """.trimIndent()
+        }
+    }
+
+    private class JsonPluginData : AutoSavePluginData("test_json") {
+        override val saveType = PluginData.SaveType.JSON
+
+        val baseMap: MutableMap<String, Base> by value()
+
+        companion object {
+            val string = """
+                {
+                    "baseMap": {
+                        "A": {
+                            "base_type": "DerivedA",
+                            "valueA": 11.4514
+                        },
+                        "B": {
+                            "base_type": "DerivedB",
+                            "valueB": "mamoe.mirai"
+                        },
+                        "C": {
+                            "base_type": "DerivedC"
+                        }
+                    }
+                }
+            """.trimIndent()
+        }
+    }
+
+    private val dataStorage by lazy { MultiFilePluginDataStorageImpl(storePath) }
+
+    @Test
+    fun testYamlLoad() {
+        val data = YamlPluginData()
+        dataStorage.load(mockPlugin, data)
+        dataStorage.getPluginDataFileInternal(mockPlugin, data).writeText(YamlPluginData.string)
+        dataStorage.load(mockPlugin, data)
+
+        assertEquals(2, data.int)
+        assertEquals(mapOf("key1" to "value1", "key2" to "value2"), data.map)
+        assertEquals(
+            mapOf(
+                "key1" to mapOf("key1" to "value1", "key2" to "value2"),
+                "key2" to mapOf("key1" to "value1", "key2" to "value2")
+            ), data.map2
+        )
+    }
+
+    @Test
+    fun testYamlStore() {
+        val data = YamlPluginData()
+        dataStorage.load(mockPlugin, data)
+
+        data.int = 2
+        data.map["key1"] = "value1"
+        data.map["key2"] = "value2"
+        data.map2["key1"] = mutableMapOf("key1" to "value1", "key2" to "value2")
+        data.map2["key2"] = mutableMapOf("key1" to "value1", "key2" to "value2")
+
+        dataStorage.store(mockPlugin, data)
+
+        val file = dataStorage.getPluginDataFileInternal(mockPlugin, data)
+        assertEquals(YamlPluginData.string, file.readText())
+    }
+
+    @Test
+    fun testJsonLoad() {
+        val data = JsonPluginData()
+        dataStorage.load(mockPlugin, data)
+        dataStorage.getPluginDataFileInternal(mockPlugin, data).writeText(JsonPluginData.string)
+        dataStorage.load(mockPlugin, data)
+
+        assertEquals(
+            mapOf(
+                "A" to DerivedA(11.4514),
+                "B" to DerivedB("mamoe.mirai"),
+                "C" to DerivedC
+            ), data.baseMap
+        )
+    }
+
+    @Test
+    fun testJsonStore() {
+        val data = JsonPluginData()
+        dataStorage.load(mockPlugin, data)
+
+        data.baseMap["A"] = DerivedA(11.4514)
+        data.baseMap["B"] = DerivedB("mamoe.mirai")
+        data.baseMap["C"] = DerivedC
+
+        dataStorage.store(mockPlugin, data)
+
+        val file = dataStorage.getPluginDataFileInternal(mockPlugin, data)
+        assertEquals(JsonPluginData.string, file.readText())
+    }
+}

+ 2 - 2
mirai-console/docs/BuiltInCommands.md

@@ -23,8 +23,8 @@ Mirai Console 内置一些指令。
 | 参数        | 可选值          | 描述                                                                  |
 |:----       | :----           | :-------------------------------------------------------------------  |
 | protocol   | ANDROID_PHONE   | Android 手机.  所有功能都支持.                                         |
-| protocol   | ANDROID_PAD     | Android 平板.  注意: 不支持戳一戳事件解析                               |
-| protocol   | ANDROID_WATCH   | Android 手表.                                                         |
+| protocol   | ANDROID_PAD     | Android 平板.                                                       |
+| protocol   | ANDROID_WATCH   | Android 手表.  注意: 不支持戳一戳事件解析                                 |
 
 
 临时登录一个账号

+ 5 - 4
mirai-console/frontend/mirai-console-frontend-base/build.gradle.kts

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
+ * 此源代码的使用受 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
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 import BinaryCompatibilityConfigurator.configureBinaryValidator
@@ -26,6 +26,7 @@ dependencies {
     compileAndTestRuntime(project(":mirai-console"))
     compileAndTestRuntime(project(":mirai-core-api"))
     compileAndTestRuntime(project(":mirai-core-utils"))
+    compileAndTestRuntime(`kotlin-jvm-blocking-bridge`)
     compileAndTestRuntime(`kotlin-stdlib-jdk8`)
 }
 

+ 2 - 1
mirai-console/frontend/mirai-console-terminal/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -24,6 +24,7 @@ dependencies {
     shadowImplementation(jline)
     shadowImplementation(jansi)
     shadowImplementation(project(":mirai-console-frontend-base"))
+    implementation(`kotlin-jvm-blocking-bridge`)
 
     testImplementation(project(":mirai-core"))
 }

+ 2 - 1
mirai-console/tools/gradle-plugin/src/main/kotlin/BuildMiraiPluginV2.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -277,6 +277,7 @@ public open class BuildMiraiPluginV2 : Jar() {
         duplicatesStrategy = DuplicatesStrategy.WARN
 
         val compilations = target.compilations.filter { it.name == KotlinCompilation.MAIN_COMPILATION_NAME }
+        @Suppress("DEPRECATION") // New API requires Kotlin 1.8.0, but we must support lower versions
         compilations.forEach {
             dependsOn(it.compileKotlinTask)
             from(it.output.allOutputs)

+ 2 - 2
mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -282,7 +282,7 @@ internal val Project.kotlinTargets: Collection<KotlinTarget>
 
         return when (kotlinExtension) {
             is KotlinMultiplatformExtension -> kotlinExtension.targets
-            is KotlinSingleTargetExtension -> listOf(kotlinExtension.target)
+            is KotlinSingleTargetExtension<*> -> listOf(kotlinExtension.target)
             else -> error("[MiraiConsole] Internal error: kotlinExtension is neither KotlinMultiplatformExtension nor KotlinSingleTargetExtension")
         }
     }

+ 4 - 3
mirai-console/tools/intellij-plugin/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -65,14 +65,15 @@ fun File.resolveMkdir(relative: String): File {
 kotlin.target.compilations.all {
     kotlinOptions {
         jvmTarget = "17"
-        apiVersion = "1.7" // bundled Kotlin is 1.7.20
+        apiVersion = "1.9" // bundled Kotlin is 1.7.20
+        languageVersion = "1.9" //  idea requires 1.9
     }
 }
 
 // https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library
 tasks.withType<org.jetbrains.intellij.tasks.PatchPluginXmlTask> {
     sinceBuild.set("223")
-    untilBuild.set("223.*")
+    untilBuild.set("231.*")
     pluginDescription.set(
         """
         Plugin development support for <a href='https://github.com/mamoe/mirai'>Mirai Console</a>

+ 3 - 3
mirai-console/tools/intellij-plugin/run/projects/test-project/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -8,8 +8,8 @@
  */
 
 plugins {
-    kotlin("jvm") version "1.7.20"
-    kotlin("plugin.serialization") version "1.7.20"
+    kotlin("jvm") version "1.8.10"
+    kotlin("plugin.serialization") version "1.8.10"
     id("net.mamoe.mirai-console") version "2.99.0-local"
     java
 }

+ 10 - 1
mirai-console/tools/intellij-plugin/run/projects/test-project/gradle/wrapper/gradle-wrapper.properties

@@ -1,5 +1,14 @@
+#
+# Copyright 2019-2023 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/dev/LICENSE
+#
+
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 4 - 2
mirai-console/tools/intellij-plugin/src/diagnostics/PluginDataValuesChecker.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.idea.inspections.collections.isCalling
 import org.jetbrains.kotlin.idea.project.builtIns
 import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
 import org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName
+import org.jetbrains.kotlin.js.descriptorUtils.getKotlinTypeFqName
 import org.jetbrains.kotlin.psi.*
 import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
 import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
@@ -109,7 +110,7 @@ class PluginDataValuesChecker : CallChecker, DeclarationChecker {
         context: CallCheckerContext
     ) {
         val classDescriptor = type.classDescriptor() ?: return
-        val jetTypeFqn = type.getJetTypeFqName(false)
+        val jetTypeFqn = type.getKotlinTypeFqName(false)
 
         val builtIns = callExpr.builtIns
         val factory = when {
@@ -194,6 +195,7 @@ private fun canBeSerializedInternally(descriptor: ClassDescriptor): Boolean {
         "kotlin.collections.Collection", "kotlin.collections.List",
         "kotlin.collections.ArrayList", "kotlin.collections.MutableList",
         -> "ArrayListSerializer"
+
         "kotlin.collections.Set", "kotlin.collections.LinkedHashSet", "kotlin.collections.MutableSet" -> "LinkedHashSetSerializer"
         "kotlin.collections.HashSet" -> "HashSetSerializer"
         "kotlin.collections.Map", "kotlin.collections.LinkedHashMap", "kotlin.collections.MutableMap" -> "LinkedHashMapSerializer"

+ 3 - 1
mirai-core-api/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -45,6 +45,8 @@ kotlin {
                 implementation(`kotlinx-serialization-protobuf`)
                 implementation(`kotlinx-atomicfu`)
                 relocateCompileOnly(`ktor-io_relocated`) // runtime from mirai-core-utils
+                implementation(`kotlin-jvm-blocking-bridge`)
+                implementation(`kotlin-dynamic-delegation`)
             }
         }
 

+ 163 - 106
mirai-core-api/compatibility-validation/android/api/android.api

@@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory {
 	public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
+	public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[B)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
@@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd
 public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory {
 	public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 }
@@ -165,6 +169,59 @@ public final class net/mamoe/mirai/_MiraiInstance {
 	public static final fun set (Lnet/mamoe/mirai/IMirai;)V
 }
 
+public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
+	public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
+	public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
+	public abstract fun getId ()J
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthResult {
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthSession {
+	public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthorization {
+	public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion;
+	public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B
+}
+
+public final class net/mamoe/mirai/auth/BotAuthorization$Companion {
+	public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization;
+}
+
+public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener {
+	public fun getQrCodeEcLevel ()I
+	public fun getQrCodeMargin ()I
+	public fun getQrCodeSize ()I
+	public fun getQrCodeStateUpdateInterval ()J
+	public fun onCompleted ()V
+	public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V
+	public fun onIntervalLoop ()V
+	public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V
+}
+
+public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum {
+	public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+}
+
 public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member {
 	public abstract fun getAnonymousId ()Ljava/lang/String;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -379,6 +436,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public abstract fun getBotAsMember ()Lnet/mamoe/mirai/contact/NormalMember;
 	public fun getBotMuteRemaining ()I
 	public fun getBotPermission ()Lnet/mamoe/mirai/contact/MemberPermission;
+	public abstract fun getEssences ()Lnet/mamoe/mirai/contact/essence/Essences;
 	public abstract fun getId ()J
 	public abstract fun getMembers ()Lnet/mamoe/mirai/contact/ContactList;
 	public abstract fun getName ()Ljava/lang/String;
@@ -745,6 +803,7 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementImage {
 	public fun equals (Ljava/lang/Object;)Z
 	public final fun getHeight ()I
 	public final fun getId ()Ljava/lang/String;
+	public final fun getUrl ()Ljava/lang/String;
 	public final fun getWidth ()I
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
@@ -753,7 +812,6 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementImage {
 
 public final class net/mamoe/mirai/contact/announcement/AnnouncementImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/contact/announcement/AnnouncementImage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/contact/announcement/AnnouncementImage;
@@ -793,7 +851,6 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementParameters {
 
 public final class net/mamoe/mirai/contact/announcement/AnnouncementParameters$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/contact/announcement/AnnouncementParameters$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/contact/announcement/AnnouncementParameters;
@@ -900,6 +957,32 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 }
 
+public final class net/mamoe/mirai/contact/essence/EssenceMessageRecord {
+	public final fun getFullSource ()Lnet/mamoe/mirai/message/data/MessageSource;
+	public final fun getFullSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getOperator ()Lnet/mamoe/mirai/contact/NormalMember;
+	public final fun getOperatorId ()J
+	public final fun getOperatorNick ()Ljava/lang/String;
+	public final fun getOperatorTime ()I
+	public final fun getSender ()Lnet/mamoe/mirai/contact/NormalMember;
+	public final fun getSenderId ()J
+	public final fun getSenderNick ()Ljava/lang/String;
+	public final fun getSenderTime ()I
+	public final fun getSource ()Lnet/mamoe/mirai/message/data/MessageSource;
+	public final fun getSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun toString ()Ljava/lang/String;
+}
+
+public abstract interface class net/mamoe/mirai/contact/essence/Essences : net/mamoe/mirai/utils/Streamable {
+	public fun getPage (II)Ljava/util/List;
+	public abstract fun getPage (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun remove (Lnet/mamoe/mirai/message/data/MessageSource;)V
+	public abstract fun remove (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun share (Lnet/mamoe/mirai/message/data/MessageSource;)Ljava/lang/String;
+	public abstract fun share (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
 public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
 	public abstract fun getExpiryTime ()J
 	public abstract fun getMd5 ()[B
@@ -1144,7 +1227,6 @@ public final class net/mamoe/mirai/data/GroupHonorType {
 
 public final class net/mamoe/mirai/data/GroupHonorType$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupHonorType$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize-NYH6FXw (Lkotlinx/serialization/encoding/Decoder;)I
@@ -1254,7 +1336,6 @@ public final class net/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequ
 
 public final class net/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest;
@@ -1298,7 +1379,6 @@ public final class net/mamoe/mirai/data/RequestEventData$MemberJoinRequest : net
 
 public final class net/mamoe/mirai/data/RequestEventData$MemberJoinRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$MemberJoinRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$MemberJoinRequest;
@@ -1329,7 +1409,6 @@ public final class net/mamoe/mirai/data/RequestEventData$NewFriendRequest : net/
 
 public final class net/mamoe/mirai/data/RequestEventData$NewFriendRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$NewFriendRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$NewFriendRequest;
@@ -3143,7 +3222,6 @@ public final class net/mamoe/mirai/message/data/At : net/mamoe/mirai/message/cod
 
 public final class net/mamoe/mirai/message/data/At$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/At$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/At;
@@ -3266,7 +3344,6 @@ public final class net/mamoe/mirai/message/data/Dice : net/mamoe/mirai/message/c
 
 public final class net/mamoe/mirai/message/data/Dice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Dice$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Dice;
@@ -3828,7 +3905,6 @@ public final class net/mamoe/mirai/message/data/Face : net/mamoe/mirai/message/c
 
 public final class net/mamoe/mirai/message/data/Face$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Face$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Face;
@@ -3897,7 +3973,6 @@ public final class net/mamoe/mirai/message/data/FlashImage : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/FlashImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/FlashImage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/FlashImage;
@@ -3947,7 +4022,6 @@ public final class net/mamoe/mirai/message/data/ForwardMessage : net/mamoe/mirai
 
 public final class net/mamoe/mirai/message/data/ForwardMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ForwardMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ForwardMessage;
@@ -3996,7 +4070,6 @@ public final class net/mamoe/mirai/message/data/ForwardMessage$Node : net/mamoe/
 
 public final class net/mamoe/mirai/message/data/ForwardMessage$Node$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ForwardMessage$Node$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ForwardMessage$Node;
@@ -4239,18 +4312,6 @@ public final class net/mamoe/mirai/message/data/ImageType : java/lang/Enum {
 	public static fun values ()[Lnet/mamoe/mirai/message/data/ImageType;
 }
 
-public final class net/mamoe/mirai/message/data/ImageType$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ImageType$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ImageType;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/ImageType;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/ImageType$Companion {
 	public final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
 	public final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
@@ -4274,7 +4335,6 @@ public final class net/mamoe/mirai/message/data/LightApp : net/mamoe/mirai/messa
 
 public final class net/mamoe/mirai/message/data/LightApp$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/LightApp$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/LightApp;
@@ -4449,7 +4509,6 @@ public abstract interface class net/mamoe/mirai/message/data/MessageMetadata : n
 
 public final class net/mamoe/mirai/message/data/MessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageOrigin$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageOrigin;
@@ -4472,18 +4531,6 @@ public final class net/mamoe/mirai/message/data/MessageOriginKind : java/lang/En
 	public static fun values ()[Lnet/mamoe/mirai/message/data/MessageOriginKind;
 }
 
-public final class net/mamoe/mirai/message/data/MessageOriginKind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageOriginKind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageOriginKind;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageOriginKind;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/MessageOriginKind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
@@ -4496,6 +4543,7 @@ public abstract class net/mamoe/mirai/message/data/MessageSource : net/mamoe/mir
 	public abstract fun getIds ()[I
 	public abstract fun getInternalIds ()[I
 	public final fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
+	public abstract fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain;
 	public abstract fun getTargetId ()J
 	public abstract fun getTime ()I
@@ -4593,18 +4641,6 @@ public final class net/mamoe/mirai/message/data/MessageSourceKind : java/lang/En
 	public static fun values ()[Lnet/mamoe/mirai/message/data/MessageSourceKind;
 }
 
-public final class net/mamoe/mirai/message/data/MessageSourceKind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSourceKind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSourceKind;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/MessageSourceKind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
@@ -4634,8 +4670,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun getContent (Lnet/mamoe/mirai/message/data/Message;)Ljava/lang/String;
 	public static final synthetic fun getIds (Lnet/mamoe/mirai/message/data/MessageChain;)[I
 	public static final synthetic fun getInternalId (Lnet/mamoe/mirai/message/data/MessageChain;)[I
-	public static final fun getKind (Lnet/mamoe/mirai/message/data/MessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
-	public static final fun getKind (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
+	public static final synthetic fun getKind (Lnet/mamoe/mirai/message/data/MessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
+	public static final synthetic fun getKind (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public static final synthetic fun getLengthDuration (Lnet/mamoe/mirai/message/data/OnlineAudio;)J
 	public static final synthetic fun getOrFail (Lnet/mamoe/mirai/message/data/MessageChain;Lnet/mamoe/mirai/message/data/MessageKey;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/SingleMessage;
 	public static synthetic fun getOrFail$default (Lnet/mamoe/mirai/message/data/MessageChain;Lnet/mamoe/mirai/message/data/MessageKey;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/SingleMessage;
@@ -4712,7 +4748,6 @@ public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/MusicShare$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MusicShare$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MusicShare;
@@ -4778,18 +4813,18 @@ public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mam
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming : net/mamoe/mirai/message/data/OnlineMessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Key;
-	public final fun getFromId ()J
+	public fun getFromId ()J
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/User;
-	public final fun getTargetId ()J
+	public fun getTargetId ()J
 }
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromFriend : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromFriend$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Friend;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4799,6 +4834,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromGroup : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromGroup$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Member;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public fun getSubject ()Lnet/mamoe/mirai/contact/Group;
@@ -4812,11 +4848,11 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromStranger : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromStranger$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Stranger;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4826,11 +4862,11 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromTemp : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromTemp$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Member;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Member;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Member;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4856,6 +4892,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$Key
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToFriend : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToFriend$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Friend;
@@ -4867,6 +4904,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToF
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToGroup : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToGroup$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Group;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Group;
@@ -4878,6 +4916,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToG
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToStranger : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToStranger$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Stranger;
@@ -4890,6 +4929,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToS
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Member;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Member;
@@ -4932,7 +4972,6 @@ public final class net/mamoe/mirai/message/data/PlainText : net/mamoe/mirai/mess
 
 public final class net/mamoe/mirai/message/data/PlainText$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/PlainText$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/PlainText;
@@ -4985,7 +5024,6 @@ public final class net/mamoe/mirai/message/data/PokeMessage : net/mamoe/mirai/me
 
 public final class net/mamoe/mirai/message/data/PokeMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/PokeMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/PokeMessage;
@@ -5022,7 +5060,6 @@ public final class net/mamoe/mirai/message/data/QuoteReply : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/QuoteReply$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/QuoteReply$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/QuoteReply;
@@ -5053,7 +5090,6 @@ public final class net/mamoe/mirai/message/data/RawForwardMessage {
 
 public final class net/mamoe/mirai/message/data/RawForwardMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/RawForwardMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RawForwardMessage;
@@ -5101,7 +5137,6 @@ public final class net/mamoe/mirai/message/data/RichMessageOrigin : net/mamoe/mi
 
 public final class net/mamoe/mirai/message/data/RichMessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/RichMessageOrigin$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RichMessageOrigin;
@@ -5160,7 +5195,6 @@ public final class net/mamoe/mirai/message/data/ShowImageFlag : net/mamoe/mirai/
 
 public final class net/mamoe/mirai/message/data/SimpleServiceMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/SimpleServiceMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/SimpleServiceMessage;
@@ -5241,7 +5275,6 @@ public final class net/mamoe/mirai/message/data/VipFace : net/mamoe/mirai/messag
 
 public final class net/mamoe/mirai/message/data/VipFace$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/VipFace$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/VipFace;
@@ -5273,7 +5306,6 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind {
 
 public final class net/mamoe/mirai/message/data/VipFace$Kind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/VipFace$Kind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/VipFace$Kind;
@@ -5308,7 +5340,6 @@ public class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/P
 
 public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice;
@@ -5357,6 +5388,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con
 	public fun getMessage ()Ljava/lang/String;
 }
 
+public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException {
+	public synthetic fun <init> (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getActual ()J
+	public final fun getExpected ()J
+}
+
 public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException {
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -5376,17 +5413,63 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
+public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException {
+	public fun <init> (Z)V
+	public fun <init> (ZLjava/lang/String;)V
+	public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
+	public fun <init> (ZLjava/lang/Throwable;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
+	public fun <init> (Ljava/lang/String;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
+public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
 public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException {
 }
 
+public abstract class net/mamoe/mirai/utils/AbstractBotConfiguration {
+	public fun <init> ()V
+	public final fun fileBasedDeviceInfo ()V
+	public final fun fileBasedDeviceInfo (Ljava/lang/String;)V
+	public static synthetic fun fileBasedDeviceInfo$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/lang/String;ILjava/lang/Object;)V
+	protected abstract fun getBotLoggerSupplier ()Lkotlin/jvm/functions/Function1;
+	public final fun getCacheDir ()Ljava/io/File;
+	protected abstract fun getDeviceInfo ()Lkotlin/jvm/functions/Function1;
+	protected abstract fun getNetworkLoggerSupplier ()Lkotlin/jvm/functions/Function1;
+	public final fun getWorkingDir ()Ljava/io/File;
+	public final fun redirectBotLogToDirectory ()V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;)V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;J)V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectBotLogToDirectory$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectBotLogToFile ()V
+	public final fun redirectBotLogToFile (Ljava/io/File;)V
+	public final fun redirectBotLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectBotLogToFile$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectNetworkLogToDirectory ()V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;)V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;J)V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectNetworkLogToDirectory$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectNetworkLogToFile ()V
+	public final fun redirectNetworkLogToFile (Ljava/io/File;)V
+	public final fun redirectNetworkLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectNetworkLogToFile$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	protected abstract fun setBotLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
+	public final fun setCacheDir (Ljava/io/File;)V
+	protected abstract fun setDeviceInfo (Lkotlin/jvm/functions/Function1;)V
+	protected abstract fun setNetworkLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
+	public final fun setWorkingDir (Ljava/io/File;)V
+}
+
 public abstract class net/mamoe/mirai/utils/AbstractExternalResource : net/mamoe/mirai/utils/ExternalResource {
 	public fun <init> ()V
 	public fun <init> (Ljava/lang/String;)V
@@ -5410,7 +5493,7 @@ public abstract interface class net/mamoe/mirai/utils/AbstractExternalResource$R
 	public abstract fun cleanup ()V
 }
 
-public class net/mamoe/mirai/utils/BotConfiguration {
+public class net/mamoe/mirai/utils/BotConfiguration : net/mamoe/mirai/utils/AbstractBotConfiguration {
 	public static final field Companion Lnet/mamoe/mirai/utils/BotConfiguration$Companion;
 	public fun <init> ()V
 	public final fun autoReconnectOnForceOffline ()V
@@ -5419,12 +5502,8 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun disableAccountSecretes ()V
 	public final fun disableContactCache ()V
 	public final fun enableContactCache ()V
-	public final fun fileBasedDeviceInfo ()V
-	public final fun fileBasedDeviceInfo (Ljava/lang/String;)V
-	public static synthetic fun fileBasedDeviceInfo$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/lang/String;ILjava/lang/Object;)V
 	public final fun getAutoReconnectOnForceOffline ()Z
 	public final fun getBotLoggerSupplier ()Lkotlin/jvm/functions/Function1;
-	public final fun getCacheDir ()Ljava/io/File;
 	public final fun getContactListCache ()Lnet/mamoe/mirai/utils/BotConfiguration$ContactListCache;
 	public static final fun getDefault ()Lnet/mamoe/mirai/utils/BotConfiguration;
 	public final fun getDeviceInfo ()Lkotlin/jvm/functions/Function1;
@@ -5441,7 +5520,6 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final synthetic fun getReconnectPeriodMillis ()J
 	public final fun getReconnectionRetryTimes ()I
 	public final fun getStatHeartbeatPeriodMillis ()J
-	public final fun getWorkingDir ()Ljava/io/File;
 	public final synthetic fun inheritCoroutineContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun isConvertLineSeparator ()Z
 	public final fun isShowingVerboseEventLog ()Z
@@ -5449,27 +5527,8 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun noBotLog ()V
 	public final fun noNetworkLog ()V
 	public final fun randomDeviceInfo ()V
-	public final fun redirectBotLogToDirectory ()V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;)V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;J)V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectBotLogToDirectory$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectBotLogToFile ()V
-	public final fun redirectBotLogToFile (Ljava/io/File;)V
-	public final fun redirectBotLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectBotLogToFile$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectNetworkLogToDirectory ()V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;)V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;J)V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectNetworkLogToDirectory$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectNetworkLogToFile ()V
-	public final fun redirectNetworkLogToFile (Ljava/io/File;)V
-	public final fun redirectNetworkLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectNetworkLogToFile$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
 	public final fun setAutoReconnectOnForceOffline (Z)V
 	public final fun setBotLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
-	public final fun setCacheDir (Ljava/io/File;)V
 	public final fun setContactListCache (Lnet/mamoe/mirai/utils/BotConfiguration$ContactListCache;)V
 	public final fun setConvertLineSeparator (Z)V
 	public final fun setDeviceInfo (Lkotlin/jvm/functions/Function1;)V
@@ -5487,7 +5546,6 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun setReconnectionRetryTimes (I)V
 	public final fun setShowingVerboseEventLog (Z)V
 	public final fun setStatHeartbeatPeriodMillis (J)V
-	public final fun setWorkingDir (Ljava/io/File;)V
 }
 
 public final class net/mamoe/mirai/utils/BotConfiguration$Companion {
@@ -5565,7 +5623,6 @@ public final class net/mamoe/mirai/utils/DeviceInfo {
 
 public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
@@ -5601,7 +5658,6 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version {
 
 public final class net/mamoe/mirai/utils/DeviceInfo$Version$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$Version$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo$Version;
@@ -5827,6 +5883,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
 	public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion;
 	public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
 	public fun <init> ()V
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;

+ 164 - 106
mirai-core-api/compatibility-validation/jvm/api/jvm.api

@@ -53,6 +53,9 @@ public abstract interface class net/mamoe/mirai/BotFactory {
 	public fun newBot (JLjava/lang/String;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
+	public abstract fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[B)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/BotFactory$BotConfigurationLambda;)Lnet/mamoe/mirai/Bot;
 	public abstract fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
@@ -65,6 +68,7 @@ public abstract interface class net/mamoe/mirai/BotFactory$BotConfigurationLambd
 public final class net/mamoe/mirai/BotFactory$INSTANCE : net/mamoe/mirai/BotFactory {
 	public final synthetic fun newBot (JLjava/lang/String;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (JLjava/lang/String;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
+	public fun newBot (JLnet/mamoe/mirai/auth/BotAuthorization;Lnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 	public final synthetic fun newBot (J[BLkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/Bot;
 	public fun newBot (J[BLnet/mamoe/mirai/utils/BotConfiguration;)Lnet/mamoe/mirai/Bot;
 }
@@ -165,6 +169,59 @@ public final class net/mamoe/mirai/_MiraiInstance {
 	public static final fun set (Lnet/mamoe/mirai/IMirai;)V
 }
 
+public abstract interface class net/mamoe/mirai/auth/BotAuthInfo {
+	public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
+	public abstract fun getDeviceInfo ()Lnet/mamoe/mirai/utils/DeviceInfo;
+	public abstract fun getId ()J
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthResult {
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthSession {
+	public abstract fun authByPassword (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByPassword ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public abstract fun authByQRCode (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract interface class net/mamoe/mirai/auth/BotAuthorization {
+	public static final field Companion Lnet/mamoe/mirai/auth/BotAuthorization$Companion;
+	public abstract fun authorize (Lnet/mamoe/mirai/auth/BotAuthSession;Lnet/mamoe/mirai/auth/BotAuthInfo;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public static fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public static fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public fun calculateSecretsKey (Lnet/mamoe/mirai/auth/BotAuthInfo;)[B
+}
+
+public final class net/mamoe/mirai/auth/BotAuthorization$Companion {
+	public final fun byPassword (Ljava/lang/String;)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byPassword ([B)Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun byQRCode ()Lnet/mamoe/mirai/auth/BotAuthorization;
+	public final fun invoke (Lkotlin/jvm/functions/Function3;)Lnet/mamoe/mirai/auth/BotAuthorization;
+}
+
+public abstract interface class net/mamoe/mirai/auth/QRCodeLoginListener {
+	public fun getQrCodeEcLevel ()I
+	public fun getQrCodeMargin ()I
+	public fun getQrCodeSize ()I
+	public fun getQrCodeStateUpdateInterval ()J
+	public fun onCompleted ()V
+	public abstract fun onFetchQRCode (Lnet/mamoe/mirai/Bot;[B)V
+	public fun onIntervalLoop ()V
+	public abstract fun onStateChanged (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;)V
+}
+
+public final class net/mamoe/mirai/auth/QRCodeLoginListener$State : java/lang/Enum {
+	public static final field CANCELLED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field CONFIRMED Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field DEFAULT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field TIMEOUT Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_CONFIRM Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static final field WAITING_FOR_SCAN Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+	public static fun values ()[Lnet/mamoe/mirai/auth/QRCodeLoginListener$State;
+}
+
 public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/mamoe/mirai/contact/Member {
 	public abstract fun getAnonymousId ()Ljava/lang/String;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -379,6 +436,7 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public abstract fun getBotAsMember ()Lnet/mamoe/mirai/contact/NormalMember;
 	public fun getBotMuteRemaining ()I
 	public fun getBotPermission ()Lnet/mamoe/mirai/contact/MemberPermission;
+	public abstract fun getEssences ()Lnet/mamoe/mirai/contact/essence/Essences;
 	public abstract fun getId ()J
 	public abstract fun getMembers ()Lnet/mamoe/mirai/contact/ContactList;
 	public abstract fun getName ()Ljava/lang/String;
@@ -745,6 +803,7 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementImage {
 	public fun equals (Ljava/lang/Object;)Z
 	public final fun getHeight ()I
 	public final fun getId ()Ljava/lang/String;
+	public final fun getUrl ()Ljava/lang/String;
 	public final fun getWidth ()I
 	public fun hashCode ()I
 	public fun toString ()Ljava/lang/String;
@@ -753,7 +812,6 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementImage {
 
 public final class net/mamoe/mirai/contact/announcement/AnnouncementImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/contact/announcement/AnnouncementImage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/contact/announcement/AnnouncementImage;
@@ -793,7 +851,6 @@ public final class net/mamoe/mirai/contact/announcement/AnnouncementParameters {
 
 public final class net/mamoe/mirai/contact/announcement/AnnouncementParameters$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/contact/announcement/AnnouncementParameters$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/contact/announcement/AnnouncementParameters;
@@ -900,6 +957,32 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt {
 	public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot;
 }
 
+public final class net/mamoe/mirai/contact/essence/EssenceMessageRecord {
+	public final fun getFullSource ()Lnet/mamoe/mirai/message/data/MessageSource;
+	public final fun getFullSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getOperator ()Lnet/mamoe/mirai/contact/NormalMember;
+	public final fun getOperatorId ()J
+	public final fun getOperatorNick ()Ljava/lang/String;
+	public final fun getOperatorTime ()I
+	public final fun getSender ()Lnet/mamoe/mirai/contact/NormalMember;
+	public final fun getSenderId ()J
+	public final fun getSenderNick ()Ljava/lang/String;
+	public final fun getSenderTime ()I
+	public final fun getSource ()Lnet/mamoe/mirai/message/data/MessageSource;
+	public final fun getSource (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun toString ()Ljava/lang/String;
+}
+
+public abstract interface class net/mamoe/mirai/contact/essence/Essences : net/mamoe/mirai/utils/Streamable {
+	public fun getPage (II)Ljava/util/List;
+	public abstract fun getPage (IILkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun remove (Lnet/mamoe/mirai/message/data/MessageSource;)V
+	public abstract fun remove (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun share (Lnet/mamoe/mirai/message/data/MessageSource;)Ljava/lang/String;
+	public abstract fun share (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
 public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder {
 	public abstract fun getExpiryTime ()J
 	public abstract fun getMd5 ()[B
@@ -1144,7 +1227,6 @@ public final class net/mamoe/mirai/data/GroupHonorType {
 
 public final class net/mamoe/mirai/data/GroupHonorType$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/GroupHonorType$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize-NYH6FXw (Lkotlinx/serialization/encoding/Decoder;)I
@@ -1254,7 +1336,6 @@ public final class net/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequ
 
 public final class net/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$BotInvitedJoinGroupRequest;
@@ -1298,7 +1379,6 @@ public final class net/mamoe/mirai/data/RequestEventData$MemberJoinRequest : net
 
 public final class net/mamoe/mirai/data/RequestEventData$MemberJoinRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$MemberJoinRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$MemberJoinRequest;
@@ -1329,7 +1409,6 @@ public final class net/mamoe/mirai/data/RequestEventData$NewFriendRequest : net/
 
 public final class net/mamoe/mirai/data/RequestEventData$NewFriendRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/data/RequestEventData$NewFriendRequest$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/data/RequestEventData$NewFriendRequest;
@@ -3143,7 +3222,6 @@ public final class net/mamoe/mirai/message/data/At : net/mamoe/mirai/message/cod
 
 public final class net/mamoe/mirai/message/data/At$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/At$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/At;
@@ -3266,7 +3344,6 @@ public final class net/mamoe/mirai/message/data/Dice : net/mamoe/mirai/message/c
 
 public final class net/mamoe/mirai/message/data/Dice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Dice$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Dice;
@@ -3828,7 +3905,6 @@ public final class net/mamoe/mirai/message/data/Face : net/mamoe/mirai/message/c
 
 public final class net/mamoe/mirai/message/data/Face$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Face$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Face;
@@ -3897,7 +3973,6 @@ public final class net/mamoe/mirai/message/data/FlashImage : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/FlashImage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/FlashImage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/FlashImage;
@@ -3947,7 +4022,6 @@ public final class net/mamoe/mirai/message/data/ForwardMessage : net/mamoe/mirai
 
 public final class net/mamoe/mirai/message/data/ForwardMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ForwardMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ForwardMessage;
@@ -3996,7 +4070,6 @@ public final class net/mamoe/mirai/message/data/ForwardMessage$Node : net/mamoe/
 
 public final class net/mamoe/mirai/message/data/ForwardMessage$Node$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ForwardMessage$Node$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ForwardMessage$Node;
@@ -4239,18 +4312,6 @@ public final class net/mamoe/mirai/message/data/ImageType : java/lang/Enum {
 	public static fun values ()[Lnet/mamoe/mirai/message/data/ImageType;
 }
 
-public final class net/mamoe/mirai/message/data/ImageType$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/ImageType$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/ImageType;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/ImageType;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/ImageType$Companion {
 	public final fun match (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
 	public final fun matchOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/ImageType;
@@ -4274,7 +4335,6 @@ public final class net/mamoe/mirai/message/data/LightApp : net/mamoe/mirai/messa
 
 public final class net/mamoe/mirai/message/data/LightApp$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/LightApp$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/LightApp;
@@ -4449,7 +4509,6 @@ public abstract interface class net/mamoe/mirai/message/data/MessageMetadata : n
 
 public final class net/mamoe/mirai/message/data/MessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageOrigin$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageOrigin;
@@ -4472,18 +4531,6 @@ public final class net/mamoe/mirai/message/data/MessageOriginKind : java/lang/En
 	public static fun values ()[Lnet/mamoe/mirai/message/data/MessageOriginKind;
 }
 
-public final class net/mamoe/mirai/message/data/MessageOriginKind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageOriginKind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageOriginKind;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageOriginKind;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/MessageOriginKind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
@@ -4496,6 +4543,7 @@ public abstract class net/mamoe/mirai/message/data/MessageSource : net/mamoe/mir
 	public abstract fun getIds ()[I
 	public abstract fun getInternalIds ()[I
 	public final fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
+	public abstract fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getOriginalMessage ()Lnet/mamoe/mirai/message/data/MessageChain;
 	public abstract fun getTargetId ()J
 	public abstract fun getTime ()I
@@ -4593,18 +4641,6 @@ public final class net/mamoe/mirai/message/data/MessageSourceKind : java/lang/En
 	public static fun values ()[Lnet/mamoe/mirai/message/data/MessageSourceKind;
 }
 
-public final class net/mamoe/mirai/message/data/MessageSourceKind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MessageSourceKind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
-	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
-	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
-	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
-	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
-	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/MessageSourceKind;)V
-	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
-}
-
 public final class net/mamoe/mirai/message/data/MessageSourceKind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
@@ -4634,8 +4670,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun getContent (Lnet/mamoe/mirai/message/data/Message;)Ljava/lang/String;
 	public static final synthetic fun getIds (Lnet/mamoe/mirai/message/data/MessageChain;)[I
 	public static final synthetic fun getInternalId (Lnet/mamoe/mirai/message/data/MessageChain;)[I
-	public static final fun getKind (Lnet/mamoe/mirai/message/data/MessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
-	public static final fun getKind (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
+	public static final synthetic fun getKind (Lnet/mamoe/mirai/message/data/MessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
+	public static final synthetic fun getKind (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public static final synthetic fun getLengthDuration (Lnet/mamoe/mirai/message/data/OnlineAudio;)J
 	public static final synthetic fun getOrFail (Lnet/mamoe/mirai/message/data/MessageChain;Lnet/mamoe/mirai/message/data/MessageKey;Lkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/SingleMessage;
 	public static synthetic fun getOrFail$default (Lnet/mamoe/mirai/message/data/MessageChain;Lnet/mamoe/mirai/message/data/MessageKey;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/SingleMessage;
@@ -4712,7 +4748,6 @@ public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/MusicShare$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/MusicShare$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/MusicShare;
@@ -4778,18 +4813,18 @@ public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mam
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming : net/mamoe/mirai/message/data/OnlineMessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Key;
-	public final fun getFromId ()J
+	public fun getFromId ()J
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/User;
-	public final fun getTargetId ()J
+	public fun getTargetId ()J
 }
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromFriend : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromFriend$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Friend;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4799,6 +4834,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromGroup : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromGroup$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Member;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public fun getSubject ()Lnet/mamoe/mirai/contact/Group;
@@ -4812,11 +4848,11 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromStranger : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromStranger$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Stranger;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4826,11 +4862,11 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$Fro
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromTemp : net/mamoe/mirai/message/data/OnlineMessageSource$Incoming {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Incoming$FromTemp$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public abstract fun getSender ()Lnet/mamoe/mirai/contact/Member;
-	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
-	public final fun getSubject ()Lnet/mamoe/mirai/contact/Member;
-	public final fun getTarget ()Lnet/mamoe/mirai/Bot;
-	public synthetic fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
+	public abstract fun getSubject ()Lnet/mamoe/mirai/contact/Member;
+	public final synthetic fun getTarget ()Lnet/mamoe/mirai/Bot;
+	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/ContactOrBot;
 	public final fun toString ()Ljava/lang/String;
 }
 
@@ -4856,6 +4892,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$Key
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToFriend : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToFriend$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Friend;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Friend;
@@ -4867,6 +4904,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToF
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToGroup : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToGroup$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Group;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Group;
@@ -4878,6 +4916,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToG
 
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToStranger : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToStranger$Key;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Stranger;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Stranger;
@@ -4890,6 +4929,7 @@ public final class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToS
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp : net/mamoe/mirai/message/data/OnlineMessageSource$Outgoing {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Outgoing$ToTemp$Key;
 	public final fun getGroup ()Lnet/mamoe/mirai/contact/Group;
+	public final fun getKind ()Lnet/mamoe/mirai/message/data/MessageSourceKind;
 	public synthetic fun getSubject ()Lnet/mamoe/mirai/contact/Contact;
 	public final fun getSubject ()Lnet/mamoe/mirai/contact/Member;
 	public abstract fun getTarget ()Lnet/mamoe/mirai/contact/Member;
@@ -4932,7 +4972,6 @@ public final class net/mamoe/mirai/message/data/PlainText : net/mamoe/mirai/mess
 
 public final class net/mamoe/mirai/message/data/PlainText$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/PlainText$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/PlainText;
@@ -4985,7 +5024,6 @@ public final class net/mamoe/mirai/message/data/PokeMessage : net/mamoe/mirai/me
 
 public final class net/mamoe/mirai/message/data/PokeMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/PokeMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/PokeMessage;
@@ -5022,7 +5060,6 @@ public final class net/mamoe/mirai/message/data/QuoteReply : net/mamoe/mirai/mes
 
 public final class net/mamoe/mirai/message/data/QuoteReply$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/QuoteReply$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/QuoteReply;
@@ -5053,7 +5090,6 @@ public final class net/mamoe/mirai/message/data/RawForwardMessage {
 
 public final class net/mamoe/mirai/message/data/RawForwardMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/RawForwardMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RawForwardMessage;
@@ -5101,7 +5137,6 @@ public final class net/mamoe/mirai/message/data/RichMessageOrigin : net/mamoe/mi
 
 public final class net/mamoe/mirai/message/data/RichMessageOrigin$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/RichMessageOrigin$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/RichMessageOrigin;
@@ -5160,7 +5195,6 @@ public final class net/mamoe/mirai/message/data/ShowImageFlag : net/mamoe/mirai/
 
 public final class net/mamoe/mirai/message/data/SimpleServiceMessage$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/SimpleServiceMessage$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/SimpleServiceMessage;
@@ -5241,7 +5275,6 @@ public final class net/mamoe/mirai/message/data/VipFace : net/mamoe/mirai/messag
 
 public final class net/mamoe/mirai/message/data/VipFace$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/VipFace$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/VipFace;
@@ -5273,7 +5306,6 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind {
 
 public final class net/mamoe/mirai/message/data/VipFace$Kind$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/VipFace$Kind$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/VipFace$Kind;
@@ -5308,7 +5340,6 @@ public class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/P
 
 public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice;
@@ -5357,6 +5388,12 @@ public final class net/mamoe/mirai/network/ForceOfflineException : java/util/con
 	public fun getMessage ()Ljava/lang/String;
 }
 
+public final class net/mamoe/mirai/network/InconsistentBotIdException : net/mamoe/mirai/network/LoginFailedException {
+	public synthetic fun <init> (JJLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getActual ()J
+	public final fun getExpected ()J
+}
+
 public abstract class net/mamoe/mirai/network/LoginFailedException : java/lang/RuntimeException {
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public synthetic fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -5376,17 +5413,63 @@ public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
+public class net/mamoe/mirai/network/UnsupportedCaptchaMethodException : net/mamoe/mirai/network/LoginFailedException {
+	public fun <init> (Z)V
+	public fun <init> (ZLjava/lang/String;)V
+	public fun <init> (ZLjava/lang/String;Ljava/lang/Throwable;)V
+	public fun <init> (ZLjava/lang/Throwable;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedQRCodeCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
+	public fun <init> (Ljava/lang/String;)V
+}
+
+public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
-public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/LoginFailedException {
+public final class net/mamoe/mirai/network/UnsupportedSmsLoginException : net/mamoe/mirai/network/UnsupportedCaptchaMethodException {
 	public fun <init> (Ljava/lang/String;)V
 }
 
 public final class net/mamoe/mirai/network/WrongPasswordException : net/mamoe/mirai/network/LoginFailedException {
 }
 
+public abstract class net/mamoe/mirai/utils/AbstractBotConfiguration {
+	public fun <init> ()V
+	public final fun fileBasedDeviceInfo ()V
+	public final fun fileBasedDeviceInfo (Ljava/lang/String;)V
+	public static synthetic fun fileBasedDeviceInfo$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/lang/String;ILjava/lang/Object;)V
+	protected abstract fun getBotLoggerSupplier ()Lkotlin/jvm/functions/Function1;
+	public final fun getCacheDir ()Ljava/io/File;
+	protected abstract fun getDeviceInfo ()Lkotlin/jvm/functions/Function1;
+	protected abstract fun getNetworkLoggerSupplier ()Lkotlin/jvm/functions/Function1;
+	public final fun getWorkingDir ()Ljava/io/File;
+	public final fun redirectBotLogToDirectory ()V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;)V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;J)V
+	public final fun redirectBotLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectBotLogToDirectory$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectBotLogToFile ()V
+	public final fun redirectBotLogToFile (Ljava/io/File;)V
+	public final fun redirectBotLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectBotLogToFile$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectNetworkLogToDirectory ()V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;)V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;J)V
+	public final fun redirectNetworkLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectNetworkLogToDirectory$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	public final fun redirectNetworkLogToFile ()V
+	public final fun redirectNetworkLogToFile (Ljava/io/File;)V
+	public final fun redirectNetworkLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
+	public static synthetic fun redirectNetworkLogToFile$default (Lnet/mamoe/mirai/utils/AbstractBotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
+	protected abstract fun setBotLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
+	public final fun setCacheDir (Ljava/io/File;)V
+	protected abstract fun setDeviceInfo (Lkotlin/jvm/functions/Function1;)V
+	protected abstract fun setNetworkLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
+	public final fun setWorkingDir (Ljava/io/File;)V
+}
+
 public abstract class net/mamoe/mirai/utils/AbstractExternalResource : net/mamoe/mirai/utils/ExternalResource {
 	public fun <init> ()V
 	public fun <init> (Ljava/lang/String;)V
@@ -5410,7 +5493,7 @@ public abstract interface class net/mamoe/mirai/utils/AbstractExternalResource$R
 	public abstract fun cleanup ()V
 }
 
-public class net/mamoe/mirai/utils/BotConfiguration {
+public class net/mamoe/mirai/utils/BotConfiguration : net/mamoe/mirai/utils/AbstractBotConfiguration {
 	public static final field Companion Lnet/mamoe/mirai/utils/BotConfiguration$Companion;
 	public fun <init> ()V
 	public final fun autoReconnectOnForceOffline ()V
@@ -5419,12 +5502,8 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun disableAccountSecretes ()V
 	public final fun disableContactCache ()V
 	public final fun enableContactCache ()V
-	public final fun fileBasedDeviceInfo ()V
-	public final fun fileBasedDeviceInfo (Ljava/lang/String;)V
-	public static synthetic fun fileBasedDeviceInfo$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/lang/String;ILjava/lang/Object;)V
 	public final fun getAutoReconnectOnForceOffline ()Z
 	public final fun getBotLoggerSupplier ()Lkotlin/jvm/functions/Function1;
-	public final fun getCacheDir ()Ljava/io/File;
 	public final fun getContactListCache ()Lnet/mamoe/mirai/utils/BotConfiguration$ContactListCache;
 	public static final fun getDefault ()Lnet/mamoe/mirai/utils/BotConfiguration;
 	public final fun getDeviceInfo ()Lkotlin/jvm/functions/Function1;
@@ -5441,7 +5520,6 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final synthetic fun getReconnectPeriodMillis ()J
 	public final fun getReconnectionRetryTimes ()I
 	public final fun getStatHeartbeatPeriodMillis ()J
-	public final fun getWorkingDir ()Ljava/io/File;
 	public final synthetic fun inheritCoroutineContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public final fun isConvertLineSeparator ()Z
 	public final fun isShowingVerboseEventLog ()Z
@@ -5449,27 +5527,8 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun noBotLog ()V
 	public final fun noNetworkLog ()V
 	public final fun randomDeviceInfo ()V
-	public final fun redirectBotLogToDirectory ()V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;)V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;J)V
-	public final fun redirectBotLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectBotLogToDirectory$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectBotLogToFile ()V
-	public final fun redirectBotLogToFile (Ljava/io/File;)V
-	public final fun redirectBotLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectBotLogToFile$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectNetworkLogToDirectory ()V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;)V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;J)V
-	public final fun redirectNetworkLogToDirectory (Ljava/io/File;JLkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectNetworkLogToDirectory$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
-	public final fun redirectNetworkLogToFile ()V
-	public final fun redirectNetworkLogToFile (Ljava/io/File;)V
-	public final fun redirectNetworkLogToFile (Ljava/io/File;Lkotlin/jvm/functions/Function1;)V
-	public static synthetic fun redirectNetworkLogToFile$default (Lnet/mamoe/mirai/utils/BotConfiguration;Ljava/io/File;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
 	public final fun setAutoReconnectOnForceOffline (Z)V
 	public final fun setBotLoggerSupplier (Lkotlin/jvm/functions/Function1;)V
-	public final fun setCacheDir (Ljava/io/File;)V
 	public final fun setContactListCache (Lnet/mamoe/mirai/utils/BotConfiguration$ContactListCache;)V
 	public final fun setConvertLineSeparator (Z)V
 	public final fun setDeviceInfo (Lkotlin/jvm/functions/Function1;)V
@@ -5487,7 +5546,6 @@ public class net/mamoe/mirai/utils/BotConfiguration {
 	public final fun setReconnectionRetryTimes (I)V
 	public final fun setShowingVerboseEventLog (Z)V
 	public final fun setStatHeartbeatPeriodMillis (J)V
-	public final fun setWorkingDir (Ljava/io/File;)V
 }
 
 public final class net/mamoe/mirai/utils/BotConfiguration$Companion {
@@ -5565,7 +5623,6 @@ public final class net/mamoe/mirai/utils/DeviceInfo {
 
 public final class net/mamoe/mirai/utils/DeviceInfo$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo;
@@ -5601,7 +5658,6 @@ public final class net/mamoe/mirai/utils/DeviceInfo$Version {
 
 public final class net/mamoe/mirai/utils/DeviceInfo$Version$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
 	public static final field INSTANCE Lnet/mamoe/mirai/utils/DeviceInfo$Version$$serializer;
-	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/utils/DeviceInfo$Version;
@@ -5827,6 +5883,7 @@ public abstract class net/mamoe/mirai/utils/LoginSolver {
 	public static final field Companion Lnet/mamoe/mirai/utils/LoginSolver$Companion;
 	public static final field Default Lnet/mamoe/mirai/utils/LoginSolver;
 	public fun <init> ()V
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
@@ -6192,6 +6249,7 @@ public final class net/mamoe/mirai/utils/StandardCharImageLoginSolver : net/mamo
 	public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Lnet/mamoe/mirai/utils/MiraiLogger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public static final fun createBlocking (Lkotlin/jvm/functions/Function0;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
 	public static final fun createBlocking (Lkotlin/jvm/functions/Function0;Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/StandardCharImageLoginSolver;
+	public fun createQRCodeLoginListener (Lnet/mamoe/mirai/Bot;)Lnet/mamoe/mirai/auth/QRCodeLoginListener;
 	public fun isSliderCaptchaSupported ()Z
 	public fun onSolveDeviceVerification (Lnet/mamoe/mirai/Bot;Lnet/mamoe/mirai/utils/DeviceVerificationRequests;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun onSolvePicCaptcha (Lnet/mamoe/mirai/Bot;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;

+ 52 - 0
mirai-core-api/src/commonMain/kotlin/BotFactory.kt

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai
 
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.utils.BotConfiguration
 import kotlin.jvm.JvmSynthetic
 
@@ -111,6 +112,53 @@ public interface BotFactory {
      */
     public fun newBot(qq: Long, passwordMd5: ByteArray): Bot = newBot(qq, passwordMd5, BotConfiguration.Default)
 
+    ///////////////////////////////////////////////////////////////////////////
+    // BotAuthorization
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 使用 [默认配置][BotConfiguration.Default] 构造 [Bot] 实例
+     *
+     * @since 2.15
+     */
+    public fun newBot(qq: Long, authorization: BotAuthorization): Bot =
+        newBot(qq, authorization, BotConfiguration.Default)
+
+    /**
+     * 使用指定的 [配置][configuration] 构造 [Bot] 实例
+     *
+     * @since 2.15
+     */
+    public fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot
+
+
+    /**
+     * 使用指定的 [配置][configuration] 构造 [Bot] 实例
+     *
+     * Kotlin:
+     * ```
+     * newBot(123, password) {
+     *     // this: BotConfiguration
+     *     fileBasedDeviceInfo()
+     * }
+     * ```
+     *
+     * Java:
+     * ```java
+     * newBot(123, password, configuration -> {
+     *     configuration.fileBasedDeviceInfo()
+     * })
+     * ```
+     *
+     * @since 2.15
+     */
+    public fun newBot(
+        qq: Long,
+        authorization: BotAuthorization,
+        configuration: BotConfigurationLambda /* = BotConfiguration.() -> Unit */
+    ): Bot = newBot(qq, authorization, configuration.run { BotConfiguration().apply { invoke() } })
+
+
     public companion object INSTANCE : BotFactory {
         override fun newBot(qq: Long, password: String, configuration: BotConfiguration): Bot {
             return Mirai.BotFactory.newBot(qq, password, configuration)
@@ -160,5 +208,9 @@ public interface BotFactory {
             passwordMd5: ByteArray,
             configuration: BotConfiguration.() -> Unit /* = BotConfiguration.() -> Unit */
         ): Bot = newBot(qq, passwordMd5, BotConfiguration().apply(configuration))
+
+        override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot {
+            return Mirai.BotFactory.newBot(qq, authorization, configuration)
+        }
     }
 }

+ 128 - 0
mirai-core-api/src/commonMain/kotlin/auth/BotAuthorization.kt

@@ -0,0 +1,128 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.auth
+
+import net.mamoe.mirai.BotFactory
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.network.LoginFailedException
+import net.mamoe.mirai.network.RetryLaterException
+import net.mamoe.mirai.utils.*
+import kotlin.jvm.JvmStatic
+
+/**
+ * Bot 的登录鉴权方式
+ *
+ * @see BotFactory.newBot
+ *
+ * @since 2.15
+ */
+public interface BotAuthorization {
+    /**
+     * 此方法控制 Bot 如何进行登录.
+     *
+     * Bot 只能使用一种登录方式, 但是可以在一种登录方式失败的时候尝试其他登录方式
+     *
+     * ## 异常类型
+     *
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 并可建议系统进行重连或停止 bot (通过 [LoginFailedException.killBot]).
+     * 例如抛出 [RetryLaterException] 可让 bot 重新进行一次登录.
+     *
+     * 抛出任意其他 [Throwable] 将视为鉴权选择器的自身错误.
+     *
+     * ## 示例代码
+     * ```kotlin
+     * override suspend fun authorize(
+     *      authComponent: BotAuthSession,
+     *      bot: BotAuthInfo,
+     * ) {
+     *      return kotlin.runCatching {
+     *          authComponent.authByQRCode()
+     *      }.recover {
+     *          authComponent.authByPassword("...")
+     *      }.getOrThrow()
+     * }
+     * ```
+     */
+    public suspend fun authorize(
+        session: BotAuthSession,
+        info: BotAuthInfo,
+    ): BotAuthResult
+
+
+    /**
+     * 计算 `cache/account.secrets` 的加密秘钥
+     */
+    public fun calculateSecretsKey(
+        bot: BotAuthInfo,
+    ): ByteArray = bot.deviceInfo.guid + bot.id.toByteArray()
+
+    public companion object {
+        @JvmStatic
+        public fun byPassword(password: String): BotAuthorization = byPassword(password.md5())
+
+        @JvmStatic
+        public fun byPassword(passwordMd5: ByteArray): BotAuthorization = factory.byPassword(passwordMd5)
+
+        @JvmStatic
+        public fun byQRCode(): BotAuthorization = factory.byQRCode()
+
+        public operator fun invoke(
+            block: suspend (BotAuthSession, BotAuthInfo) -> BotAuthResult
+        ): BotAuthorization {
+            return object : BotAuthorization {
+                override suspend fun authorize(
+                    session: BotAuthSession,
+                    info: BotAuthInfo
+                ): BotAuthResult {
+                    return block(session, info)
+                }
+            }
+        }
+
+        private val factory: DefaultBotAuthorizationFactory by lazy {
+            Mirai // Ensure services loaded
+            loadService()
+        }
+    }
+}
+
+@NotStableForInheritance
+public interface BotAuthResult
+
+@NotStableForInheritance
+public interface BotAuthInfo {
+    public val id: Long
+    public val deviceInfo: DeviceInfo
+    public val configuration: BotConfiguration
+}
+
+@NotStableForInheritance
+public interface BotAuthSession {
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByPassword(password: String): BotAuthResult
+
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByPassword(passwordMd5: ByteArray): BotAuthResult
+
+    /**
+     * @throws LoginFailedException
+     */
+    public suspend fun authByQRCode(): BotAuthResult
+}
+
+
+internal interface DefaultBotAuthorizationFactory {
+    fun byPassword(passwordMd5: ByteArray): BotAuthorization
+    fun byQRCode(): BotAuthorization
+}

+ 101 - 0
mirai-core-api/src/commonMain/kotlin/auth/QRCodeLoginListener.kt

@@ -0,0 +1,101 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.auth
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.network.LoginFailedException
+
+/**
+ * 二维码扫描登录监听器
+ *
+ * @since 2.15
+ */
+public interface QRCodeLoginListener {
+
+    /**
+     * 使用二维码登录时获取的二维码图片大小字节数.
+     */
+    public val qrCodeSize: Int get() = 3
+
+    /**
+     * 使用二维码登录时获取的二维码边框宽度像素.
+     */
+    public val qrCodeMargin: Int get() = 4
+
+    /**
+     * 使用二维码登录时获取的二维码校正等级,必须为 1-3 之间.
+     */
+    public val qrCodeEcLevel: Int get() = 2
+
+    /**
+     * 每隔 [qrCodeStateUpdateInterval] 毫秒更新一次[二维码状态][State]
+     */
+    public val qrCodeStateUpdateInterval: Long get() = 5000
+
+    /**
+     * 从服务器获取二维码时调用,在下级显示二维码并扫描.
+     *
+     * @param data 二维码图像数据 (文件)
+     */
+    public fun onFetchQRCode(bot: Bot, data: ByteArray)
+
+    /**
+     * 当二维码状态变化时调用.
+     * @see State
+     */
+    public fun onStateChanged(bot: Bot, state: State)
+
+    /**
+     * 每隔一段时间会调用一次此函数
+     *
+     * 在此函数抛出 [LoginFailedException] 以中断登录
+     */
+    public fun onIntervalLoop() {
+    }
+
+    /**
+     * 当二维码登录扫描完毕时执行, 在此执行资源释放
+     */
+    public fun onCompleted() {
+    }
+
+    public enum class State {
+        /**
+         * 等待扫描中,请在此阶段请扫描二维码.
+         * @see QRCodeLoginListener.onFetchQRCode
+         */
+        WAITING_FOR_SCAN,
+
+        /**
+         * 二维码已扫描,等待扫描端确认登录.
+         */
+        WAITING_FOR_CONFIRM,
+
+        /**
+         * 扫描后取消了确认.
+         */
+        CANCELLED,
+
+        /**
+         * 二维码超时,必须重新获取二维码.
+         */
+        TIMEOUT,
+
+        /**
+         * 二维码已确认,将会继续登录.
+         */
+        CONFIRMED,
+
+        /**
+         * 默认状态,在登录前通常为此状态.
+         */
+        DEFAULT,
+    }
+}

+ 9 - 1
mirai-core-api/src/commonMain/kotlin/contact/Group.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -17,6 +17,7 @@ import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.active.GroupActive
 import net.mamoe.mirai.contact.announcement.Announcements
+import net.mamoe.mirai.contact.essence.Essences
 import net.mamoe.mirai.contact.file.RemoteFiles
 import net.mamoe.mirai.contact.roaming.RoamingSupported
 import net.mamoe.mirai.event.events.*
@@ -229,6 +230,13 @@ public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported,
      */
     public suspend fun setEssenceMessage(source: MessageSource): Boolean
 
+    /**
+     * 群精华消息相关功能接口
+     *
+     * @since 2.15
+     */
+    public val essences: Essences
+
     public companion object {
         /**
          * 将一条消息设置为群精华消息, 需要管理员或群主权限.

+ 6 - 1
mirai-core-api/src/commonMain/kotlin/contact/announcement/AnnouncementImage.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -31,6 +31,11 @@ public class AnnouncementImage private constructor(
 ) {
     // For stability, do not make it `data class`.
 
+    /**
+     * @since 2.15
+     */
+    public val url: String get() = "https://gdynamic.qpic.cn/gdynamic/$id/628"
+
     public companion object {
         public const val SERIAL_NAME: String = "AnnouncementImage"
 

+ 70 - 0
mirai-core-api/src/commonMain/kotlin/contact/essence/EssenceMessageRecord.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.contact.essence
+
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.NormalMember
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.utils.MiraiInternalApi
+
+/**
+ * 精华消息记录
+ * @since 2.15
+ * @param group 记录的群聊
+ * @param sender 消息的发送者
+ * @param senderId 消息的发送者的ID
+ * @param senderNick 消息的发送者的Nick
+ * @param senderTime 消息的发送的时间 *
+ * @param operator 设置精华的操作者
+ * @param operatorId 设置精华的操作者的ID
+ * @param operatorNick 设置精华的操作者的Nick
+ * @param operatorTime 设置精华的时间
+ */
+public class EssenceMessageRecord @MiraiInternalApi constructor(
+    public val group: Group,
+    public val sender: NormalMember?,
+    public val senderId: Long,
+    public val senderNick: String,
+    public val senderTime: Int,
+    public val operator: NormalMember?,
+    public val operatorId: Long,
+    public val operatorNick: String,
+    public val operatorTime: Int,
+    private val loadMessageSource: suspend (parse: Boolean) -> MessageSource
+) {
+    override fun toString(): String {
+        return "EssenceMessageRecord(group=${group}, sender=${senderNick}(${senderId}), senderTime=${senderTime}, operator=${operatorNick}(${operatorId}), operatorTime=${operatorTime})"
+    }
+
+    /**
+     * 获取消息源
+     *
+     * 其中的 [MessageSource.originalMessage] 将会尝试以加载为原消息格式
+     *
+     * **注意** 当精华消息中包含 图片 时,会尝试将其下载然后重新上传, 以保证可用性
+     *
+     * @see getSource
+     */
+    @JvmBlockingBridge
+    public suspend fun getFullSource(): MessageSource {
+        return loadMessageSource(true)
+    }
+
+    /**
+     * 获取消息源
+     *
+     * @see getFullSource
+     */
+    @JvmBlockingBridge
+    public suspend fun getSource(): MessageSource {
+        return loadMessageSource(false)
+    }
+}

+ 69 - 0
mirai-core-api/src/commonMain/kotlin/contact/essence/Essences.kt

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.contact.essence
+
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.utils.NotStableForInheritance
+import net.mamoe.mirai.utils.Streamable
+
+/**
+ * 表示一个群精华消息管理.
+ *
+ * ## 获取 [Essences] 实例
+ *
+ * 只可以通过 [Group.essences] 获取一个群的精华消息管理, 即 [Essences] 实例.
+ *
+ * ### 获取精华消息列表
+ *
+ * 通过 [asFlow] 或 `asStream` 可以获取到*惰性*流, 在从流中收集数据时才会请求服务器获取数据. 通常建议在 Kotlin 使用协程的 [asFlow], 在 Java 使用 `asStream`.
+ *
+ * 若要获取全部精华消息列表, 可使用 [toList].
+ *
+ * ### 获取精华消息分享链接
+ *
+ * 通过 [share] 可以获得一个精华消息的分享链接
+ *
+ * ### 移除精华消息
+ *
+ * 通过 [remove] 可以从列表中移除指定精华消息 (WEB API)
+ *
+ * @since 2.15
+ */
+@NotStableForInheritance
+public interface Essences : Streamable<EssenceMessageRecord> {
+
+    /**
+     * 按页获取精华消息记录
+     * @param start 起始索引 从 0 开始
+     * @param limit 页大小 返回的记录最大数量,最大取 50
+     * @throws IllegalStateException [limit] 过大或其他参数错误时会触发异常
+     */
+    @JvmBlockingBridge
+    public suspend fun getPage(start: Int, limit: Int): List<EssenceMessageRecord>
+
+    /**
+     * 分享精华消息
+     * @param source 要分享的消息源
+     * @throws IllegalStateException [source] 不为精华消息时将会触发异常
+     * @return 分享 URL
+     */
+    @JvmBlockingBridge
+    public suspend fun share(source: MessageSource): String
+
+    /**
+     * 移除精华消息
+     * @throws IllegalStateException [source] 不为精华消息或权限不足时将会触发异常
+     * @param source 要移除的消息源
+     */
+    @JvmBlockingBridge
+    public suspend fun remove(source: MessageSource)
+}

+ 4 - 4
mirai-core-api/src/commonMain/kotlin/internal/message/AbstractPolymorphicSerializer.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -77,14 +77,14 @@ internal abstract class AbstractPolymorphicSerializer<T : Any> internal construc
     open fun findPolymorphicSerializerOrNull(
         decoder: CompositeDecoder,
         klassName: String?
-    ): DeserializationStrategy<out T>? = decoder.serializersModule.getPolymorphic(baseClass, klassName)
+    ): DeserializationStrategy<T>? = decoder.serializersModule.getPolymorphic(baseClass, klassName)
 
 
     /**
      * Lookups an actual serializer for given [value] within the current [base class][baseClass].
      * May use context from the [encoder].
      */
-    public open fun findPolymorphicSerializerOrNull(
+    open fun findPolymorphicSerializerOrNull(
         encoder: Encoder,
         value: T
     ): SerializationStrategy<T>? =
@@ -95,7 +95,7 @@ internal abstract class AbstractPolymorphicSerializer<T : Any> internal construc
 internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(
     decoder: CompositeDecoder,
     klassName: String?
-): DeserializationStrategy<out T> =
+): DeserializationStrategy<T> =
     findPolymorphicSerializerOrNull(decoder, klassName) ?: throwSubtypeNotRegistered(klassName, baseClass)
 
 internal fun <T : Any> AbstractPolymorphicSerializer<T>.findPolymorphicSerializer(

+ 17 - 18
mirai-core-api/src/commonMain/kotlin/message/data/MessageSource.kt

@@ -207,6 +207,13 @@ public sealed class MessageSource : Message, MessageMetadata, ConstrainSingle {
      */
     public abstract val isOriginalMessageInitialized: Boolean
 
+    /**
+     * 消息种类
+     *
+     * @since 2.15
+     */
+    public abstract val kind: MessageSourceKind
+
     public abstract override fun toString(): String
 
     @MiraiInternalApi
@@ -376,26 +383,18 @@ public enum class MessageSourceKind {
     STRANGER
 }
 
-/**
- * 获取 [MessageSourceKind]
+/*
+  public static final net.mamoe.mirai.message.data.MessageSourceKind getKind(net.mamoe.mirai.message.data.MessageSource);
+  public static final net.mamoe.mirai.message.data.MessageSourceKind getKind(net.mamoe.mirai.message.data.OnlineMessageSource);
  */
-public val MessageSource.kind: MessageSourceKind
-    get() = when (this) {
-        is OnlineMessageSource -> kind
-        is OfflineMessageSource -> kind
-    }
+@JvmName("getKind")
+@Deprecated("For ABI compatibility", level = DeprecationLevel.HIDDEN)
+public fun getKindLegacy(source: MessageSource): MessageSourceKind = source.kind
+
+@JvmName("getKind")
+@Deprecated("For ABI compatibility", level = DeprecationLevel.HIDDEN)
+public fun getKindLegacy(source: OnlineMessageSource): MessageSourceKind = source.kind
 
-/**
- * 获取 [MessageSourceKind]
- */
-public val OnlineMessageSource.kind: MessageSourceKind
-    get() = when (subject) {
-        is Group -> MessageSourceKind.GROUP
-        is Friend -> MessageSourceKind.FRIEND
-        is Member -> MessageSourceKind.TEMP
-        is Stranger -> MessageSourceKind.STRANGER
-        else -> error("Internal error: OnlineMessageSource.kind reached an unexpected clause, subject=$subject")
-    }
 
 // For MessageChain, no need to expose to Java.
 

+ 1 - 1
mirai-core-api/src/commonMain/kotlin/message/data/OfflineMessageSource.kt

@@ -37,7 +37,7 @@ public abstract class OfflineMessageSource : MessageSource() {
     /**
      * 消息种类
      */
-    public abstract val kind: MessageSourceKind
+    public abstract override val kind: MessageSourceKind
 
     final override fun toString(): String {
         return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from $fromId to $targetId at $time]"

+ 69 - 9
mirai-core-api/src/commonMain/kotlin/message/data/OnlineMessageSource.kt

@@ -101,6 +101,8 @@ public sealed class OnlineMessageSource : MessageSource() {
             public abstract override val target: Friend
             public final override val subject: Friend get() = target
 
+            final override val kind: MessageSourceKind get() = MessageSourceKind.FRIEND
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from $fromId to friend $targetId at $time]"
             }
@@ -114,6 +116,8 @@ public sealed class OnlineMessageSource : MessageSource() {
             public abstract override val target: Stranger
             public final override val subject: Stranger get() = target
 
+            final override val kind: MessageSourceKind get() = MessageSourceKind.STRANGER
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from $fromId to stranger $targetId at $time]"
             }
@@ -127,6 +131,8 @@ public sealed class OnlineMessageSource : MessageSource() {
             public val group: Group get() = target.group
             public final override val subject: Member get() = target
 
+            final override val kind: MessageSourceKind get() = MessageSourceKind.TEMP
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from $fromId to group temp $targetId at $time]"
             }
@@ -139,6 +145,8 @@ public sealed class OnlineMessageSource : MessageSource() {
             public abstract override val target: Group
             public final override val subject: Group get() = target
 
+            final override val kind: MessageSourceKind get() = MessageSourceKind.GROUP
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from $fromId to group $targetId at $time]"
             }
@@ -149,19 +157,37 @@ public sealed class OnlineMessageSource : MessageSource() {
      * 接收到的一条消息的 [MessageSource]
      */
     public sealed class Incoming : OnlineMessageSource() {
+        /**
+         * 当 [sender] 为 [bot] 自身时为 bot 的对应表示 (如: [Bot.asFriend], [Bot.asStranger], [Group.botAsMember])
+         */
         public abstract override val sender: User
 
-        public final override val fromId: Long get() = sender.id
-        public final override val targetId: Long get() = target.id
+        /// NOTE: DONT use final to avoid contact not available
+        public override val fromId: Long get() = sender.id
+        public override val targetId: Long get() = target.id
 
         @NotStableForInheritance
         public abstract class FromFriend @MiraiInternalApi constructor() : Incoming() {
             public companion object Key :
                 AbstractPolymorphicMessageKey<Incoming, FromFriend>(Incoming, { it.safeCast() })
 
+            public abstract override val subject: Friend
+
+            /**
+             * 当 [sender] 为 [bot] 自身时为 [Bot.asFriend]
+             */
             public abstract override val sender: Friend
-            public final override val subject: Friend get() = sender
-            public final override val target: Bot get() = sender.bot
+            public abstract override val target: ContactOrBot
+
+            @JvmName("getTarget")
+            @Deprecated("For ABI compatibility", level = DeprecationLevel.HIDDEN)
+            public fun getTargetLegacy(): Bot {
+                if (targetId == bot.id) return subject.bot
+
+                error("Message target isn't bot; $this")
+            }
+
+            final override val kind: MessageSourceKind get() = MessageSourceKind.FRIEND
 
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from friend $fromId to $targetId at $time]"
@@ -173,10 +199,25 @@ public sealed class OnlineMessageSource : MessageSource() {
             public companion object Key :
                 AbstractPolymorphicMessageKey<Incoming, FromTemp>(Incoming, { it.safeCast() })
 
+            /**
+             * 当 [sender] 为 [bot] 自身时为 [Group.botAsMember]
+             */
             public abstract override val sender: Member
-            public inline val group: Group get() = sender.group
-            public final override val subject: Member get() = sender
-            public final override val target: Bot get() = sender.bot
+            public abstract override val subject: Member
+            public abstract override val target: ContactOrBot
+
+            public inline val group: Group get() = subject.group
+
+            @JvmName("getTarget")
+            @Deprecated("For ABI compatibility", level = DeprecationLevel.HIDDEN)
+            public fun getTargetLegacy(): Bot {
+                if (targetId == bot.id) return subject.bot
+
+                error("Message target isn't bot; $this")
+            }
+
+            final override val kind: MessageSourceKind get() = MessageSourceKind.TEMP
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from group temp $fromId to $targetId at $time]"
             }
@@ -187,9 +228,23 @@ public sealed class OnlineMessageSource : MessageSource() {
             public companion object Key :
                 AbstractPolymorphicMessageKey<Incoming, FromStranger>(Incoming, { it.safeCast() })
 
+            /**
+             * 当 [sender] 为 [bot] 自身时为 [Bot.asStranger]
+             */
             public abstract override val sender: Stranger
-            public final override val subject: Stranger get() = sender
-            public final override val target: Bot get() = sender.bot
+
+            public abstract override val subject: Stranger
+            public abstract override val target: ContactOrBot
+
+            @JvmName("getTarget")
+            @Deprecated("For ABI compatibility", level = DeprecationLevel.HIDDEN)
+            public fun getTargetLegacy(): Bot {
+                if (targetId == bot.id) return subject.bot
+
+                error("Message target isn't bot; $this")
+            }
+
+            final override val kind: MessageSourceKind get() = MessageSourceKind.STRANGER
 
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from stranger $fromId to $targetId at $time]"
@@ -201,11 +256,16 @@ public sealed class OnlineMessageSource : MessageSource() {
             public companion object Key :
                 AbstractPolymorphicMessageKey<Incoming, FromGroup>(Incoming, { it.safeCast() })
 
+            /**
+             * 当 [sender] 为 [bot] 自身时为 [Group.botAsMember]
+             */
             public abstract override val sender: Member
             public override val subject: Group get() = sender.group
             public final override val target: Group get() = subject
             public inline val group: Group get() = subject
 
+            final override val kind: MessageSourceKind get() = MessageSourceKind.GROUP
+
             final override fun toString(): String {
                 return "[mirai:source:ids=${ids.contentToString()}, internalIds=${internalIds.contentToString()}, from group $fromId to $targetId at $time]"
             }

+ 37 - 3
mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -37,6 +37,21 @@ public class WrongPasswordException @MiraiInternalApi constructor(
     message: String?
 ) : LoginFailedException(true, message)
 
+/**
+ * 二维码扫码账号与 BOT 账号不一致。
+ *
+ * @since 2.15
+ */
+public class InconsistentBotIdException @MiraiInternalApi constructor(
+    public val expected: Long,
+    public val actual: Long,
+    message: String? = null
+) : LoginFailedException(
+    true,
+    message
+        ?: "trying to logging in a bot whose id is different from the one provided to BotFactory.newBot, expected=$expected, actual=$actual."
+)
+
 /**
  * 无可用服务器
  */
@@ -60,16 +75,35 @@ public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
     public override val cause: Throwable? = null
 ) : LoginFailedException(true, "no standard input for captcha")
 
+/**
+ * 当前 [LoginSolver] 不支持此验证方式
+ *
+ * @since 2.15
+ */
+public open class UnsupportedCaptchaMethodException : LoginFailedException {
+    public constructor(killBot: Boolean) : super(killBot)
+    public constructor(killBot: Boolean, message: String?) : super(killBot, message)
+    public constructor(killBot: Boolean, message: String?, cause: Throwable?) : super(killBot, message, cause)
+    public constructor(killBot: Boolean, cause: Throwable?) : super(killBot, cause = cause)
+}
+
 /**
  * 需要强制短信验证, 且当前 [LoginSolver] 不支持时抛出.
  * @since 2.13
  */
-public class UnsupportedSmsLoginException(message: String?) : LoginFailedException(true, message)
+public class UnsupportedSmsLoginException(message: String?) : UnsupportedCaptchaMethodException(true, message)
 
 /**
  * 无法完成滑块验证
  */
-public class UnsupportedSliderCaptchaException(message: String?) : LoginFailedException(true, message)
+public class UnsupportedSliderCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message)
+
+/**
+ * 需要二维码登录, 且当前 [LoginSolver] 不支持时抛出
+ *
+ * @since 2.15
+ */
+public class UnsupportedQRCodeCaptchaException(message: String?) : UnsupportedCaptchaMethodException(true, message)
 
 /**
  * 非 mirai 实现的异常

+ 38 - 0
mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import net.mamoe.mirai.Bot
+import kotlin.jvm.JvmOverloads
+
+
+/**
+ * [BotConfiguration] 的平台特别配置
+ * @since 2.15
+ */
+@NotStableForInheritance
+public expect abstract class AbstractBotConfiguration @MiraiInternalApi protected constructor() {
+    protected abstract var deviceInfo: ((Bot) -> DeviceInfo)?
+    protected abstract var networkLoggerSupplier: ((Bot) -> MiraiLogger)
+    protected abstract var botLoggerSupplier: ((Bot) -> MiraiLogger)
+
+    /**
+     * 使用文件存储设备信息.
+     *
+     * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常.
+     * @param filepath 文件路径. 默认是相对于 `workingDir` 的文件 "device.json".
+     * @see BotConfiguration.deviceInfo
+     */
+    @JvmOverloads
+    @BotConfiguration.ConfigurationDsl
+    public fun fileBasedDeviceInfo(filepath: String = "device.json")
+
+    internal fun applyMppCopy(new: BotConfiguration)
+}

+ 124 - 57
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -17,17 +17,22 @@ package net.mamoe.mirai.utils
 
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
+import kotlinx.serialization.json.Json
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.BotFactory
 import net.mamoe.mirai.event.events.BotOfflineEvent
 import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.jvm.*
 import kotlin.native.CName
 import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
 
 /**
- * [Bot] 配置. 用于 [BotFactory.newBot]
+ * [Bot] 配置. 用于 [BotFactory.newBot].
+ *
+ * 部分平台相关配置位于 [AbstractBotConfiguration], 例如 `fileBasedDeviceInfo`.
  *
  * Kotlin 使用方法:
  * ```
@@ -50,13 +55,13 @@ import kotlin.time.Duration
  * ```
  */
 @Suppress("PropertyName")
-public expect open class BotConfiguration() { // open for Java
+public open class BotConfiguration : AbstractBotConfiguration() { // open for Java
     ///////////////////////////////////////////////////////////////////////////
     // Coroutines
     ///////////////////////////////////////////////////////////////////////////
 
     /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
-    public var parentCoroutineContext: CoroutineContext
+    public var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
 
     /**
      * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
@@ -112,7 +117,9 @@ public expect open class BotConfiguration() { // open for Java
      */
     @JvmSynthetic
     @ConfigurationDsl
-    public suspend inline fun inheritCoroutineContext()
+    public suspend inline fun inheritCoroutineContext() {
+        parentCoroutineContext = coroutineContext
+    }
 
 
     ///////////////////////////////////////////////////////////////////////////
@@ -120,7 +127,7 @@ public expect open class BotConfiguration() { // open for Java
     ///////////////////////////////////////////////////////////////////////////
 
     /** 连接心跳包周期. 过长会导致被服务器断开连接. */
-    public var heartbeatPeriodMillis: Long
+    public var heartbeatPeriodMillis: Long = 60.secondsToMillis
 
     /**
      * 状态心跳包周期. 过长会导致掉线.
@@ -128,13 +135,13 @@ public expect open class BotConfiguration() { // open for Java
      * @since 2.6
      * @see heartbeatStrategy
      */
-    public var statHeartbeatPeriodMillis: Long
+    public var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
 
     /**
      * 心跳策略.
      * @since 2.6.3
      */
-    public var heartbeatStrategy: HeartbeatStrategy
+    public var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
 
     /**
      * 心跳策略.
@@ -168,7 +175,7 @@ public expect open class BotConfiguration() { // open for Java
      * 每次心跳时等待结果的时间.
      * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
      */
-    public var heartbeatTimeoutMillis: Long
+    public var heartbeatTimeoutMillis: Long = 5.secondsToMillis
 
     /** 心跳失败后的第一次重连前的等待时间. */
     @Deprecated(
@@ -176,7 +183,7 @@ public expect open class BotConfiguration() { // open for Java
         level = DeprecationLevel.HIDDEN
     ) // deprecated since 2.7, error since 2.8
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public var firstReconnectDelayMillis: Long
+    public var firstReconnectDelayMillis: Long = 5.secondsToMillis
 
     /** 重连失败后, 继续尝试的每次等待时间 */
     @Deprecated(
@@ -184,10 +191,10 @@ public expect open class BotConfiguration() { // open for Java
         level = DeprecationLevel.HIDDEN
     ) // deprecated since 2.7, error since 2.8
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public var reconnectPeriodMillis: Long
+    public var reconnectPeriodMillis: Long = 5.secondsToMillis
 
     /** 最多尝试多少次重连 */
-    public var reconnectionRetryTimes: Int
+    public var reconnectionRetryTimes: Int = Int.MAX_VALUE
 
     /**
      * 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
@@ -196,7 +203,7 @@ public expect open class BotConfiguration() { // open for Java
      *
      * @since 2.1
      */
-    public var autoReconnectOnForceOffline: Boolean
+    public var autoReconnectOnForceOffline: Boolean = false
 
     /**
      * 验证码处理器
@@ -208,10 +215,10 @@ public expect open class BotConfiguration() { // open for Java
      *
      * @see LoginSolver
      */
-    public var loginSolver: LoginSolver?
+    public var loginSolver: LoginSolver? = LoginSolver.Default
 
     /** 使用协议类型 */
-    public var protocol: MiraiProtocol
+    public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
 
     public enum class MiraiProtocol {
         /**
@@ -221,13 +228,13 @@ public expect open class BotConfiguration() { // open for Java
 
         /**
          * Android 平板.
-         *
-         * 注意: 不支持戳一戳事件解析
          */
         ANDROID_PAD,
 
         /**
          * Android 手表.
+         *
+         * 注意: 不支持戳一戳事件解析
          */
         ANDROID_WATCH,
 
@@ -251,25 +258,27 @@ public expect open class BotConfiguration() { // open for Java
      * Highway 通道上传图片, 语音, 文件等资源时的协程数量.
      *
      * 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
-     * 默认 [CPU 核心数][Runtime.availableProcessors].
+     * 默认 CPU 核心数.
      *
      * @since 2.2
      */
-    public var highwayUploadCoroutineCount: Int
+    public var highwayUploadCoroutineCount: Int = availableProcessors()
 
     /**
      * 设置 [autoReconnectOnForceOffline] 为 `true`, 即在被挤下线时自动重连.
      * @since 2.1
      */
     @ConfigurationDsl
-    public fun autoReconnectOnForceOffline()
+    public fun autoReconnectOnForceOffline() {
+        autoReconnectOnForceOffline = true
+    }
 
     ///////////////////////////////////////////////////////////////////////////
     // Device
     ///////////////////////////////////////////////////////////////////////////
 
     @JvmField
-    internal var accountSecrets: Boolean
+    internal var accountSecrets: Boolean = true
 
     /**
      * 禁止保存 `account.secrets`.
@@ -279,14 +288,16 @@ public expect open class BotConfiguration() { // open for Java
      *
      * @since 2.11
      */
-    public fun disableAccountSecretes()
+    public fun disableAccountSecretes() {
+        accountSecrets = false
+    }
 
     /**
      * 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
      * @see fileBasedDeviceInfo 使用指定文件存储设备信息
      * @see randomDeviceInfo 使用随机设备信息
      */
-    public var deviceInfo: ((Bot) -> DeviceInfo)?
+    public final override var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
 
     /**
      * 使用随机设备信息.
@@ -294,7 +305,9 @@ public expect open class BotConfiguration() { // open for Java
      * @see deviceInfo
      */
     @ConfigurationDsl
-    public fun randomDeviceInfo()
+    public fun randomDeviceInfo() {
+        deviceInfo = null
+    }
 
     /**
      * 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
@@ -302,18 +315,11 @@ public expect open class BotConfiguration() { // open for Java
      * @see deviceInfo
      */
     @ConfigurationDsl
-    public fun loadDeviceInfoJson(json: String)
-
-    /**
-     * 使用文件存储设备信息.
-     *
-     * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常.
-     * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
-     * @see deviceInfo
-     */
-    @JvmOverloads
-    @ConfigurationDsl
-    public fun fileBasedDeviceInfo(filepath: String = "device.json")
+    public fun loadDeviceInfoJson(json: String) {
+        deviceInfo = {
+            DeviceInfoManager.deserialize(json, Companion.json)
+        }
+    }
 
     ///////////////////////////////////////////////////////////////////////////
     // Logging
@@ -322,33 +328,39 @@ public expect open class BotConfiguration() { // open for Java
     /**
      * 日志记录器
      *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
+     * - 默认打印到标准输出, 通过 [MiraiLogger.Factory.create]
      * - 忽略所有日志: [noBotLog]
      * - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
      * - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
      *
      * @see MiraiLogger
      */
-    public var botLoggerSupplier: ((Bot) -> MiraiLogger)
+    public final override var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
+        MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
+    }
 
     /**
      * 网络层日志构造器
      *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
+     * - 默认打印到标准输出, 通过 [MiraiLogger.Factory.create]
      * - 忽略所有日志: [noNetworkLog]
      * - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
      * - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
      *
      * @see MiraiLogger
      */
-    public var networkLoggerSupplier: ((Bot) -> MiraiLogger)
+    public final override var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
+        MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
+    }
 
     /**
      * 不显示网络日志. 不推荐.
      * @see networkLoggerSupplier 更多日志处理方式
      */
     @ConfigurationDsl
-    public fun noNetworkLog()
+    public fun noNetworkLog() {
+        networkLoggerSupplier = { _ -> SilentLogger }
+    }
 
 
     /**
@@ -356,7 +368,10 @@ public expect open class BotConfiguration() { // open for Java
      * @see botLoggerSupplier 更多日志处理方式
      */
     @ConfigurationDsl
-    public fun noBotLog()
+    public fun noBotLog() {
+        botLoggerSupplier = { _ -> SilentLogger }
+    }
+
 
     /**
      * 是否显示过于冗长的事件日志
@@ -365,17 +380,17 @@ public expect open class BotConfiguration() { // open for Java
      *
      * @since 2.8
      */
-    public var isShowingVerboseEventLog: Boolean
+    public var isShowingVerboseEventLog: Boolean = false
 
     ///////////////////////////////////////////////////////////////////////////
     // Cache
     //////////////////////////////////////////////////////////////////////////
 
     /**
-     * 联系人信息缓存配置. 将会保存在 [cacheDir] 中 `contacts` 目录
+     * 联系人信息缓存配置. 将会保存在 `cacheDir` 中 `contacts` 目录
      * @since 2.4
      */
-    public var contactListCache: ContactListCache
+    public var contactListCache: ContactListCache = ContactListCache()
 
     /**
      * 联系人信息缓存配置
@@ -388,24 +403,29 @@ public expect open class BotConfiguration() { // open for Java
         /**
          * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
          */
-        public var saveIntervalMillis: Long
+        public var saveIntervalMillis: Long = 60_000
 
         /**
          * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
          */ // was @ExperimentalTime before 2.9
-        public var saveInterval: Duration
+        public inline var saveInterval: Duration
+            @JvmSynthetic inline get() = saveIntervalMillis.milliseconds
+            @JvmSynthetic inline set(v) {
+                saveIntervalMillis = v.inWholeMilliseconds
+            }
 
         /**
          * 开启好友列表缓存.
          */
-        public var friendListCacheEnabled: Boolean
+        public var friendListCacheEnabled: Boolean = false
 
         /**
          * 开启群成员列表缓存.
          */
-        public var groupMemberListCacheEnabled: Boolean
+        public var groupMemberListCacheEnabled: Boolean = false
     }
 
+
     /**
      * 配置 [ContactListCache]
      * ```
@@ -417,21 +437,31 @@ public expect open class BotConfiguration() { // open for Java
      * @since 2.4
      */
     @JvmSynthetic
-    public inline fun contactListCache(action: ContactListCache.() -> Unit)
+    public inline fun contactListCache(action: ContactListCache.() -> Unit) {
+        action.invoke(this.contactListCache)
+    }
 
     /**
      * 禁用好友列表和群成员列表的缓存.
      * @since 2.4
      */
     @ConfigurationDsl
-    public fun disableContactCache()
+    public fun disableContactCache() {
+        contactListCache.friendListCacheEnabled = false
+        contactListCache.groupMemberListCacheEnabled = false
+    }
+
 
     /**
      * 启用好友列表和群成员列表的缓存.
      * @since 2.4
      */
     @ConfigurationDsl
-    public fun enableContactCache()
+    public fun enableContactCache() {
+        contactListCache.friendListCacheEnabled = true
+        contactListCache.groupMemberListCacheEnabled = true
+    }
+
 
     /**
      * 登录缓存.
@@ -445,14 +475,37 @@ public expect open class BotConfiguration() { // open for Java
      *
      * @since 2.6
      */
-    public var loginCacheEnabled: Boolean
+    public var loginCacheEnabled: Boolean = true
 
     ///////////////////////////////////////////////////////////////////////////
     // Misc
     ///////////////////////////////////////////////////////////////////////////
 
     @Suppress("DuplicatedCode")
-    public fun copy(): BotConfiguration
+    public fun copy(): BotConfiguration {
+        return BotConfiguration().also { new ->
+            // To structural order
+            new.parentCoroutineContext = parentCoroutineContext
+            new.heartbeatPeriodMillis = heartbeatPeriodMillis
+            new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
+            new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
+            new.heartbeatStrategy = heartbeatStrategy
+            new.reconnectionRetryTimes = reconnectionRetryTimes
+            new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
+            new.loginSolver = loginSolver
+            new.protocol = protocol
+            new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
+            new.accountSecrets = accountSecrets
+            new.deviceInfo = deviceInfo
+            new.botLoggerSupplier = botLoggerSupplier
+            new.networkLoggerSupplier = networkLoggerSupplier
+            new.contactListCache = contactListCache
+            new.convertLineSeparator = convertLineSeparator
+            new.isShowingVerboseEventLog = isShowingVerboseEventLog
+
+            applyMppCopy(new)
+        }
+    }
 
     /**
      * 是否处理接受到的特殊换行符, 默认为 `true`
@@ -463,17 +516,31 @@ public expect open class BotConfiguration() { // open for Java
      * @since 2.4
      */
     @get:JvmName("isConvertLineSeparator")
-    public var convertLineSeparator: Boolean
+    public var convertLineSeparator: Boolean = true
 
     /** 标注一个配置 DSL 函数 */
     @Target(AnnotationTarget.FUNCTION)
     @DslMarker
-    public annotation class ConfigurationDsl()
+    public annotation class ConfigurationDsl
 
     public companion object {
         /** 默认的配置实例. 可以进行修改 */
         @JvmStatic
-        public val Default: BotConfiguration
+        public val Default: BotConfiguration = BotConfiguration()
+
+        /**
+         * Json 序列化器, 使用 'kotlinx.serialization'
+         */
+        internal val json: Json = kotlin.runCatching {
+            Json {
+                isLenient = true
+                ignoreUnknownKeys = true
+                prettyPrint = true
+            }
+        }.getOrElse {
+            @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
+            (Json {})
+        }
     }
 }
 

+ 18 - 1
mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -13,8 +13,12 @@ package net.mamoe.mirai.utils
 
 import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.BotAuthSession
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.auth.QRCodeLoginListener
 import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.network.RetryLaterException
+import net.mamoe.mirai.network.UnsupportedQRCodeCaptchaException
 import net.mamoe.mirai.network.UnsupportedSmsLoginException
 import net.mamoe.mirai.utils.LoginSolver.Companion.Default
 import kotlin.jvm.JvmField
@@ -49,6 +53,19 @@ public abstract class LoginSolver {
      */
     public open val isSliderCaptchaSupported: Boolean get() = PlatformLoginSolverImplementations.isSliderCaptchaSupported
 
+    /**
+     * 当使用二维码登录时会通过此方法创建二维码登录监听器
+     *
+     * @see QRCodeLoginListener
+     * @see BotAuthorization
+     * @see BotAuthSession.authByQRCode
+     *
+     * @since 2.15
+     */
+    public open fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener {
+        throw UnsupportedQRCodeCaptchaException("This login session requires QRCode login, but current LoginSolver($this) does not support it. Please override `LoginSolver.createQRCodeLoginListener`.")
+    }
+
     /**
      * 处理滑动验证码.
      *

+ 22 - 519
mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -7,324 +7,33 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:Suppress("unused", "DEPRECATION_ERROR", "EXPOSED_SUPER_CLASS", "MemberVisibilityCanBePrivate")
-
-@file:JvmMultifileClass
-@file:JvmName("Utils")
-
-
 package net.mamoe.mirai.utils
 
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.serialization.json.Json
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.BotFactory
-import net.mamoe.mirai.event.events.BotOfflineEvent
 import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
 import java.io.File
 import java.io.InputStream
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
 
 /**
- * [Bot] 配置. 用于 [BotFactory.newBot]
- *
- * Kotlin 使用方法:
- * ```
- * val bot = BotFactory.newBot(...) {
- *    // 在这里配置 Bot
- *
- *    bogLoggerSupplier = { bot -> ... }
- *    fileBasedDeviceInfo()
- *    inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
- * }
- * ```
- *
- * Java 使用方法:
- * ```java
- * Bot bot = BotFactory.newBot(..., new BotConfiguration() {{
- *     setBogLoggerSupplier((Bot bot) -> { ... })
- *     fileBasedDeviceInfo()
- *     ...
- * }})
- * ```
+ * [BotConfiguration] 的 JVM 平台特别配置
+ * @since 2.15
  */
-@Suppress("PropertyName")
-public actual open class BotConfiguration { // open for Java
-    /**
-     * 工作目录. 默认为 "."
-     */
-    public var workingDir: File = File(".")
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Coroutines
-    ///////////////////////////////////////////////////////////////////////////
-
-    /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
-    public actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
-
-    /**
-     * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
-     *
-     * Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job]
-     *
-     * 用例:
-     * ```
-     * coroutineScope {
-     *   val bot = Bot(...) {
-     *     inheritCoroutineContext()
-     *   }
-     *   bot.login()
-     * } // coroutineScope 会等待 Bot 退出
-     * ```
-     *
-     *
-     * **注意**: `bot.cancel` 时将会让父 [Job] 也被 cancel.
-     * ```
-     * coroutineScope { // this: CoroutineScope
-     *   launch {
-     *     while(isActive) {
-     *       delay(500)
-     *       println("I'm alive")
-     *     }
-     *   }
-     *
-     *   val bot = Bot(...) {
-     *      inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
-     *   }
-     *   bot.login()
-     *   bot.cancel() // 取消了整个 `coroutineScope`, 因此上文不断打印 `"I'm alive"` 的协程也会被取消.
-     * }
-     * ```
-     *
-     * 因此, 此函数尤为适合在 `suspend fun main()` 中使用, 它能阻止主线程退出:
-     * ```
-     * suspend fun main() {
-     *   val bot = Bot() {
-     *     inheritCoroutineContext()
-     *   }
-     *   bot.eventChannel.subscribe { ... }
-     *
-     *   // 主线程不会退出, 直到 Bot 离线.
-     * }
-     * ```
-     *
-     * 简言之,
-     * - 若想让 [Bot] 作为 '守护进程' 运行, 则无需调用 [inheritCoroutineContext].
-     * - 若想让 [Bot] 依赖于当前协程, 让当前协程等待 [Bot] 运行, 则使用 [inheritCoroutineContext]
-     *
-     * @see parentCoroutineContext
-     */
-    @JvmSynthetic
-    @ConfigurationDsl
-    public actual suspend inline fun inheritCoroutineContext() {
-        parentCoroutineContext = coroutineContext
-    }
-
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Connection
-    ///////////////////////////////////////////////////////////////////////////
-
-    /** 连接心跳包周期. 过长会导致被服务器断开连接. */
-    public actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
-
-    /**
-     * 状态心跳包周期. 过长会导致掉线.
-     * 该值会在登录时根据服务器下发的配置自动进行更新.
-     * @since 2.6
-     * @see heartbeatStrategy
-     */
-    public actual var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
-
-    /**
-     * 心跳策略.
-     * @since 2.6.3
-     */
-    public actual var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
-
-    /**
-     * 心跳策略.
-     * @since 2.6.3
-     */
-    public actual enum class HeartbeatStrategy {
-        /**
-         * 使用 2.6.0 增加的*状态心跳* (Stat Heartbeat). 通常推荐这个模式.
-         *
-         * 该模式大多数情况下更稳定. 但有些账号使用这个模式时会遇到一段时间后发送消息成功但客户端不可见的问题.
-         */
-        STAT_HB,
-
-        /**
-         * 不发送状态心跳, 而是发送*切换在线状态* (可能会导致频繁的好友或客户端上线提示, 也可能产生短暂 (几秒) 发送消息不可见的问题).
-         *
-         * 建议在 [STAT_HB] 不可用时使用 [REGISTER].
-         */
-        REGISTER,
-
-        /**
-         * 不主动维护会话. 多数账号会每 16 分钟掉线然后重连. 则会有短暂的不可用时间.
-         *
-         * 仅当 [STAT_HB] 和 [REGISTER] 都造成无法接收等问题时使用.
-         * 同时请在 [https://github.com/mamoe/mirai/issues/1209] 提交问题.
-         */
-        NONE;
-    }
+@NotStableForInheritance
+public actual abstract class AbstractBotConfiguration { // open for Java
+    protected actual abstract var deviceInfo: ((Bot) -> DeviceInfo)?
+    protected actual abstract var networkLoggerSupplier: ((Bot) -> MiraiLogger)
+    protected actual abstract var botLoggerSupplier: ((Bot) -> MiraiLogger)
 
-    /**
-     * 每次心跳时等待结果的时间.
-     * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
-     */
-    public actual var heartbeatTimeoutMillis: Long = 5.secondsToMillis
-
-    /** 心跳失败后的第一次重连前的等待时间. */
-    @Deprecated(
-        "Useless since new network. Please just remove this.",
-        level = DeprecationLevel.HIDDEN
-    ) // deprecated since 2.7, error since 2.8
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
-
-    /** 重连失败后, 继续尝试的每次等待时间 */
-    @Deprecated(
-        "Useless since new network. Please just remove this.",
-        level = DeprecationLevel.HIDDEN
-    ) // deprecated since 2.7, error since 2.8
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public actual var reconnectPeriodMillis: Long = 5.secondsToMillis
-
-    /** 最多尝试多少次重连 */
-    public actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
-
-    /**
-     * 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
-     *
-     * 其他情况掉线都默认会自动重连, 详见 [BotOfflineEvent.reconnect]
-     *
-     * @since 2.1
-     */
-    public actual var autoReconnectOnForceOffline: Boolean = false
-
-    /**
-     * 验证码处理器
-     *
-     * - 在 Android 需要手动提供 [LoginSolver]
-     * - 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
-     *
-     * 详见 [LoginSolver.Default]
-     *
-     * @see LoginSolver
-     */
-    public actual var loginSolver: LoginSolver? = LoginSolver.Default
-
-    /** 使用协议类型 */
-    public actual var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
-
-    public actual enum class MiraiProtocol {
-        /**
-         * Android 手机. 所有功能都支持.
-         */
-        ANDROID_PHONE,
-
-        /**
-         * Android 平板.
-         *
-         * 注意: 不支持戳一戳事件解析
-         */
-        ANDROID_PAD,
-
-        /**
-         * Android 手表.
-         */
-        ANDROID_WATCH,
-
-        /**
-         * iPad - 来自MiraiGo
-         *
-         * @since 2.8
-         */
-        IPAD,
-
-        /**
-         * MacOS - 来自MiraiGo
-         *
-         * @since 2.8
-         */
-        MACOS,
-
-    }
 
     /**
-     * Highway 通道上传图片, 语音, 文件等资源时的协程数量.
-     *
-     * 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
-     * 默认 [CPU 核心数][Runtime.availableProcessors].
-     *
-     * @since 2.2
-     */
-    public actual var highwayUploadCoroutineCount: Int = Runtime.getRuntime().availableProcessors()
-
-    /**
-     * 设置 [autoReconnectOnForceOffline] 为 `true`, 即在被挤下线时自动重连.
-     * @since 2.1
+     * 工作目录. 默认为 "."
      */
-    @ConfigurationDsl
-    public actual fun autoReconnectOnForceOffline() {
-        autoReconnectOnForceOffline = true
-    }
+    public var workingDir: File = File(".")
 
     ///////////////////////////////////////////////////////////////////////////
     // Device
     ///////////////////////////////////////////////////////////////////////////
 
-    @JvmField
-    internal actual var accountSecrets: Boolean = true
-
-    /**
-     * 禁止保存 `account.secrets`.
-     *
-     * `account.secrets` 保存账号的会话信息。
-     * 它可加速登录过程,也可能可以减少出现验证码的次数。如果遇到一段时间后无法接收消息通知等同步问题时可尝试禁用。
-     *
-     * @since 2.11
-     */
-    public actual fun disableAccountSecretes() {
-        accountSecrets = false
-    }
-
-    /**
-     * 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
-     * @see fileBasedDeviceInfo 使用指定文件存储设备信息
-     * @see randomDeviceInfo 使用随机设备信息
-     */
-    public actual var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
-
-    /**
-     * 使用随机设备信息.
-     *
-     * @see deviceInfo
-     */
-    @ConfigurationDsl
-    public actual fun randomDeviceInfo() {
-        deviceInfo = null
-    }
-
-    /**
-     * 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
-     *
-     * @see deviceInfo
-     */
-    @ConfigurationDsl
-    public actual fun loadDeviceInfoJson(json: String) {
-        deviceInfo = {
-            DeviceInfoManager.deserialize(json, Companion.json)
-        }
-    }
-
     /**
      * 使用文件存储设备信息.
      *
@@ -333,43 +42,17 @@ public actual open class BotConfiguration { // open for Java
      * @see deviceInfo
      */
     @JvmOverloads
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public actual fun fileBasedDeviceInfo(filepath: String) {
-        deviceInfo = getFileBasedDeviceInfoSupplier { workingDir.resolve(filepath) }
+        deviceInfo = {
+            workingDir.resolve(filepath).loadAsDeviceInfo(BotConfiguration.json)
+        }
     }
 
     ///////////////////////////////////////////////////////////////////////////
     // Logging
     ///////////////////////////////////////////////////////////////////////////
 
-    /**
-     * 日志记录器
-     *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
-     * - 忽略所有日志: [noBotLog]
-     * - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
-     * - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
-     *
-     * @see MiraiLogger
-     */
-    public actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
-    }
-
-    /**
-     * 网络层日志构造器
-     *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
-     * - 忽略所有日志: [noNetworkLog]
-     * - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
-     * - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
-     *
-     * @see MiraiLogger
-     */
-    public actual var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
-    }
-
 
     /**
      * 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
@@ -378,7 +61,7 @@ public actual open class BotConfiguration { // open for Java
      * @see redirectNetworkLogToDirectory
      */
     @JvmOverloads
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public fun redirectNetworkLogToDirectory(
         dir: File = File("logs"),
         retain: Long = 1.weeksToMillis,
@@ -395,7 +78,7 @@ public actual open class BotConfiguration { // open for Java
      * @see redirectNetworkLogToDirectory
      */
     @JvmOverloads
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public fun redirectNetworkLogToFile(
         file: File = File("mirai.log"),
         identity: (bot: Bot) -> String = { "Net ${it.id}" }
@@ -411,7 +94,7 @@ public actual open class BotConfiguration { // open for Java
      * @see redirectBotLogToDirectory
      */
     @JvmOverloads
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public fun redirectBotLogToFile(
         file: File = File("mirai.log"),
         identity: (bot: Bot) -> String = { "Bot ${it.id}" }
@@ -427,7 +110,7 @@ public actual open class BotConfiguration { // open for Java
      * @see redirectBotLogToFile
      */
     @JvmOverloads
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public fun redirectBotLogToDirectory(
         dir: File = File("logs"),
         retain: Long = 1.weeksToMillis,
@@ -437,33 +120,6 @@ public actual open class BotConfiguration { // open for Java
         botLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) }
     }
 
-    /**
-     * 不显示网络日志. 不推荐.
-     * @see networkLoggerSupplier 更多日志处理方式
-     */
-    @ConfigurationDsl
-    public actual fun noNetworkLog() {
-        networkLoggerSupplier = { _ -> SilentLogger }
-    }
-
-    /**
-     * 不显示 [Bot] 日志. 不推荐.
-     * @see botLoggerSupplier 更多日志处理方式
-     */
-    @ConfigurationDsl
-    public actual fun noBotLog() {
-        botLoggerSupplier = { _ -> SilentLogger }
-    }
-
-    /**
-     * 是否显示过于冗长的事件日志
-     *
-     * 默认为 `false`
-     *
-     * @since 2.8
-     */
-    public actual var isShowingVerboseEventLog: Boolean = false
-
     ///////////////////////////////////////////////////////////////////////////
     // Cache
     //////////////////////////////////////////////////////////////////////////
@@ -485,165 +141,12 @@ public actual open class BotConfiguration { // open for Java
      */
     public var cacheDir: File = File("cache")
 
-    /**
-     * 联系人信息缓存配置. 将会保存在 [cacheDir] 中 `contacts` 目录
-     * @since 2.4
-     */
-    public actual var contactListCache: ContactListCache = ContactListCache()
-
-    /**
-     * 联系人信息缓存配置
-     * @see contactListCache
-     * @see enableContactCache
-     * @see disableContactCache
-     * @since 2.4
-     */
-    public actual class ContactListCache {
-        /**
-         * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
-         */
-        public actual var saveIntervalMillis: Long = 60_000
-
-        /**
-         * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
-         */ // was @ExperimentalTime before 2.9
-        public actual inline var saveInterval: Duration
-            @JvmSynthetic inline get() = saveIntervalMillis.milliseconds
-            @JvmSynthetic inline set(v) {
-                saveIntervalMillis = v.inWholeMilliseconds
-            }
-
-        /**
-         * 开启好友列表缓存.
-         */
-        public actual var friendListCacheEnabled: Boolean = false
-
-        /**
-         * 开启群成员列表缓存.
-         */
-        public actual var groupMemberListCacheEnabled: Boolean = false
-    }
-
-    /**
-     * 配置 [ContactListCache]
-     * ```
-     * contactListCache {
-     *     saveIntervalMillis = 30_000
-     *     friendListCacheEnabled = true
-     * }
-     * ```
-     * @since 2.4
-     */
-    @JvmSynthetic
-    public actual inline fun contactListCache(action: ContactListCache.() -> Unit) {
-        action.invoke(this.contactListCache)
-    }
-
-    /**
-     * 禁用好友列表和群成员列表的缓存.
-     * @since 2.4
-     */
-    @ConfigurationDsl
-    public actual fun disableContactCache() {
-        contactListCache.friendListCacheEnabled = false
-        contactListCache.groupMemberListCacheEnabled = false
-    }
-
-    /**
-     * 启用好友列表和群成员列表的缓存.
-     * @since 2.4
-     */
-    @ConfigurationDsl
-    public actual fun enableContactCache() {
-        contactListCache.friendListCacheEnabled = true
-        contactListCache.groupMemberListCacheEnabled = true
-    }
-
-    /**
-     * 登录缓存.
-     *
-     * 开始后在密码登录成功时会保存秘钥等信息, 在下次启动时通过这些信息登录, 而不提交密码.
-     * 可以减少验证码出现的频率.
-     *
-     * 秘钥信息会由密码加密保存. 如果秘钥过期, 则会进行普通密码登录.
-     *
-     * 默认 `true` (开启).
-     *
-     * @since 2.6
-     */
-    public actual var loginCacheEnabled: Boolean = true
-
     ///////////////////////////////////////////////////////////////////////////
     // Misc
     ///////////////////////////////////////////////////////////////////////////
 
-    @Suppress("DuplicatedCode")
-    public actual fun copy(): BotConfiguration {
-        return BotConfiguration().also { new ->
-            // To structural order
-            new.workingDir = workingDir
-            @Suppress("DEPRECATION_ERROR")
-            new.parentCoroutineContext = parentCoroutineContext
-            new.heartbeatPeriodMillis = heartbeatPeriodMillis
-            new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
-            new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
-            new.heartbeatStrategy = heartbeatStrategy
-            new.reconnectionRetryTimes = reconnectionRetryTimes
-            new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
-            new.loginSolver = loginSolver
-            new.protocol = protocol
-            new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
-            new.accountSecrets = accountSecrets
-            new.deviceInfo = deviceInfo
-            new.botLoggerSupplier = botLoggerSupplier
-            new.networkLoggerSupplier = networkLoggerSupplier
-            new.cacheDir = cacheDir
-            new.contactListCache = contactListCache
-            new.convertLineSeparator = convertLineSeparator
-            new.isShowingVerboseEventLog = isShowingVerboseEventLog
-        }
-    }
-
-    /**
-     * 是否处理接受到的特殊换行符, 默认为 `true`
-     *
-     * - 若为 `true`, 会将收到的 `CRLF(\r\n)` 和 `CR(\r)` 替换为 `LF(\n)`
-     * - 若为 `false`, 则不做处理
-     *
-     * @since 2.4
-     */
-    @get:JvmName("isConvertLineSeparator")
-    public actual var convertLineSeparator: Boolean = true
-
-    /** 标注一个配置 DSL 函数 */
-    @Target(AnnotationTarget.FUNCTION)
-    @DslMarker
-    public actual annotation class ConfigurationDsl
-
-    public actual companion object {
-        /** 默认的配置实例. 可以进行修改 */
-        @JvmStatic
-        public actual val Default: BotConfiguration = BotConfiguration()
-
-        /**
-         * Json 序列化器, 使用 'kotlinx.serialization'
-         */
-        internal val json: Json = kotlin.runCatching {
-            Json {
-                isLenient = true
-                ignoreUnknownKeys = true
-                prettyPrint = true
-            }
-        }.getOrElse {
-            @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
-            Json {}
-        }
-
-        internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
-            return {
-                @Suppress("DEPRECATION_ERROR")
-                file().loadAsDeviceInfo(json)
-            }
-        }
+    internal actual fun applyMppCopy(new: BotConfiguration) {
+        new.workingDir = workingDir
+        new.cacheDir = cacheDir
     }
-}
+}

+ 164 - 7
mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -17,6 +17,7 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.QRCodeLoginListener
 import net.mamoe.mirai.network.NoStandardInputForCaptchaException
 import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
 import java.awt.Image
@@ -57,6 +58,105 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
     }
 
     override val isSliderCaptchaSupported: Boolean get() = true
+    override fun createQRCodeLoginListener(bot: Bot): QRCodeLoginListener {
+        return object : QRCodeLoginListener {
+            private var tmpFile: File? = null
+
+            override val qrCodeMargin: Int get() = 1
+            override val qrCodeSize: Int get() = 1
+
+            override fun onFetchQRCode(bot: Bot, data: ByteArray) {
+                val logger = loggerSupplier(bot)
+
+                logger.info { "[QRCodeLogin] 已获取登录二维码,请在手机 QQ 使用账号 ${bot.id} 扫码" }
+                logger.info { "[QRCodeLogin] Fetched login qrcode, please scan via qq android with account ${bot.id}." }
+
+                try {
+                    val tempFile: File
+                    if (tmpFile == null) {
+                        tempFile = File.createTempFile(
+                            "mirai-qrcode-${bot.id}-${currentTimeSeconds()}",
+                            ".png"
+                        ).apply { deleteOnExit() }
+
+                        tempFile.createNewFile()
+
+                        tmpFile = tempFile
+                    } else {
+                        tempFile = tmpFile!!
+                    }
+
+                    tempFile.writeBytes(data)
+                    logger.info { "[QRCodeLogin] 将会显示二维码图片,若看不清图片,请查看文件 file://${tempFile.absolutePath}" }
+                    logger.info { "[QRCodeLogin] Displaying qrcode image. If not clear, view file file://${tempFile.absolutePath}." }
+                } catch (e: Exception) {
+                    logger.warning("[QRCodeLogin] 无法写出二维码图片. 请尽量关闭终端个性化样式后扫描二维码字符图片", e)
+                    logger.warning(
+                        "[QRCodeLogin] Failed to export qrcode image. Please try to scan the char-image after disabling custom terminal style.",
+                        e
+                    )
+                }
+
+                data.inputStream().use { stream ->
+                    try {
+                        val isCacheEnabled = ImageIO.getUseCache()
+
+                        try {
+                            ImageIO.setUseCache(false)
+                            val img = ImageIO.read(stream)
+                            if (img == null) {
+                                logger.warning { "[QRCodeLogin] 无法创建字符图片. 请查看文件" }
+                                logger.warning { "[QRCodeLogin] Failed to create char-image. Please see the file." }
+                            } else {
+                                logger.info { "[QRCodeLogin] \n" + img.renderQRCode() }
+                            }
+                        } finally {
+                            ImageIO.setUseCache(isCacheEnabled)
+                        }
+
+                    } catch (throwable: Throwable) {
+                        logger.warning("[QRCodeLogin] 创建字符图片时出错. 请查看文件.", throwable)
+                        logger.warning("[QRCodeLogin] Failed to create char-image. Please see the file.", throwable)
+                    }
+                }
+            }
+
+            override fun onStateChanged(bot: Bot, state: QRCodeLoginListener.State) {
+                val logger = loggerSupplier(bot)
+                logger.info {
+                    buildString {
+                        append("[QRCodeLogin] ")
+                        when (state) {
+                            QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("等待扫描二维码中")
+                            QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("扫描完成,请在手机 QQ 确认登录")
+                            QRCodeLoginListener.State.CANCELLED -> append("已取消登录,将会重新获取二维码")
+                            QRCodeLoginListener.State.TIMEOUT -> append("扫描超时,将会重新获取二维码")
+                            QRCodeLoginListener.State.CONFIRMED -> append("已确认登录")
+                            else -> append("default state")
+                        }
+                    }
+                }
+                logger.info {
+                    buildString {
+                        append("[QRCodeLogin] ")
+                        when (state) {
+                            QRCodeLoginListener.State.WAITING_FOR_SCAN -> append("Waiting for scanning qrcode.")
+                            QRCodeLoginListener.State.WAITING_FOR_CONFIRM -> append("Scan complete. Please confirm login.")
+                            QRCodeLoginListener.State.CANCELLED -> append("Login cancelled, we will try to fetch qrcode again.")
+                            QRCodeLoginListener.State.TIMEOUT -> append("Timeout scanning, we will try to fetch qrcode again.")
+                            QRCodeLoginListener.State.CONFIRMED -> append("Login confirmed.")
+                            else -> append("default state")
+                        }
+                    }
+                }
+
+                if (state == QRCodeLoginListener.State.CONFIRMED) {
+                    kotlin.runCatching { tmpFile?.delete() }.onFailure { logger.warning(it) }
+                }
+            }
+
+        }
+    }
 
     override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
         val logger = loggerSupplier(bot)
@@ -67,8 +167,8 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
             logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
             try {
                 tempFile.writeBytes(data)
-                logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
-                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
+                logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 file://${tempFile.absolutePath}" }
+                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file file://${tempFile.absolutePath}." }
             } catch (e: Exception) {
                 logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
                 logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
@@ -103,8 +203,11 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
         logger.info { "[SliderCaptcha] Slider captcha required. Please solve the captcha with following link. Type ticket here after completion." }
         logger.info { "[SliderCaptcha] @see https://github.com/project-mirai/mirai-login-solver-selenium" }
         logger.info { "[SliderCaptcha] @see https://docs.mirai.mamoe.net/mirai-login-solver-selenium/" }
-        logger.info { "[SliderCaptcha] 或者输入 TxCaptchaHelper 来使用 TxCaptchaHelper 完成滑动验证码" }
-        logger.info { "[SliderCaptcha] Or type `TxCaptchaHelper` to resolve slider captcha with TxCaptchaHelper.apk" }
+        logger.info { "[SliderCaptcha] 或者输入 helper 来使用 TxCaptchaHelper 完成滑动验证码" }
+        logger.info { "[SliderCaptcha] Or type helper to resolve slider captcha with TxCaptchaHelper.apk" }
+        logger.warning { "[SliderCaptcha] TxCaptchaHelper 的在线服务疑似被屏蔽,可能无法使用。TxCaptchaHelper 现已无法满足登录QQ机器人,请在以下链接下载全新的验证器" }
+        logger.warning { "[SliderCaptcha] The service of TxCaptchaHelper might be blocked. We recommend you to download the new login solver plugin in below link." }
+        logger.warning { "[SliderCaptcha] @see https://github.com/KasukuSakura/mirai-login-solver-sakura" }
         logger.info { "[SliderCaptcha] Captcha link: $url" }
 
         suspend fun runTxCaptchaHelper(): String {
@@ -126,7 +229,7 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
         }
 
         return input().also {
-            if (it == "TxCaptchaHelper" || it == "`TxCaptchaHelper`") {
+            if (it == "TxCaptchaHelper" || it == "`TxCaptchaHelper`" || it == "helper" || it == "`helper`") {
                 return runTxCaptchaHelper()
             }
             logger.info { "[SliderCaptcha] 正在提交中..." }
@@ -278,4 +381,58 @@ private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Doub
             append(line.substring(minXPos, maxXPos)).append("\n")
         }
     }
-}
+}
+
+private fun BufferedImage.renderQRCode(
+    blackPlaceholder: String = "   ",
+    whitePlaceholder: String = "   ",
+    doColorSwitch: Boolean = true,
+): String {
+    var lastStatus: Boolean? = null
+
+    fun isBlackBlock(rgb: Int): Boolean {
+        val r = rgb and 0xff0000 shr 16
+        val g = rgb and 0x00ff00 shr 8
+        val b = rgb and 0x0000ff
+
+        return r < 10 && g < 10 && b < 10
+    }
+
+    val sb = StringBuilder()
+    sb.append("\n")
+
+    val BLACK = "\u001b[30;40m"
+    val WHITE = "\u001b[97;107m"
+    val RESET = "\u001b[0m"
+
+    for (y in 0 until height) {
+        for (x in 0 until width) {
+            val rgbcolor = getRGB(x, y)
+            val crtStatus = isBlackBlock(rgbcolor)
+
+            if (doColorSwitch && crtStatus != lastStatus) {
+                lastStatus = crtStatus
+                sb.append(
+                    if (crtStatus) BLACK else WHITE
+                )
+            }
+
+            sb.append(
+                if (crtStatus) blackPlaceholder else whitePlaceholder
+            )
+        }
+
+        if (doColorSwitch) {
+            sb.append(RESET)
+        }
+
+        sb.append("\n")
+        lastStatus = null
+    }
+
+    if (doColorSwitch) {
+        sb.append(RESET)
+    }
+
+    return sb.toString()
+}

+ 15 - 508
mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -7,317 +7,26 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:Suppress("RedundantVisibilityModifier")
-
 package net.mamoe.mirai.utils
 
 import io.ktor.utils.io.core.*
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.serialization.json.Json
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.BotFactory
-import net.mamoe.mirai.event.events.BotOfflineEvent
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
 
 /**
- * [Bot] 配置. 用于 [BotFactory.newBot]
- *
- * Kotlin 使用方法:
- * ```
- * val bot = BotFactory.newBot(...) {
- *    // 在这里配置 Bot
- *
- *    bogLoggerSupplier = { bot -> ... }
- *    fileBasedDeviceInfo()
- *    inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
- * }
- * ```
- *
- * Java 使用方法:
- * ```java
- * Bot bot = BotFactory.newBot(..., new BotConfiguration() {{
- *     setBogLoggerSupplier((Bot bot) -> { ... })
- *     fileBasedDeviceInfo()
- *     ...
- * }})
- * ```
+ * [BotConfiguration] 的 Native 平台特别配置
+ * @since 2.15
  */
-@Suppress("PropertyName")
-public actual open class BotConfiguration { // open for Java
+@NotStableForInheritance
+public actual abstract class AbstractBotConfiguration { // open for Java
+    protected actual abstract var deviceInfo: ((Bot) -> DeviceInfo)?
+    protected actual abstract var networkLoggerSupplier: ((Bot) -> MiraiLogger)
+    protected actual abstract var botLoggerSupplier: ((Bot) -> MiraiLogger)
+
     /**
      * 工作目录. 默认为当前目录
      */
     public var workingDir: String = "."
 
-    ///////////////////////////////////////////////////////////////////////////
-    // Coroutines
-    ///////////////////////////////////////////////////////////////////////////
-
-    /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
-    public actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
-
-    /**
-     * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
-     *
-     * Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job]
-     *
-     * 用例:
-     * ```
-     * coroutineScope {
-     *   val bot = Bot(...) {
-     *     inheritCoroutineContext()
-     *   }
-     *   bot.login()
-     * } // coroutineScope 会等待 Bot 退出
-     * ```
-     *
-     *
-     * **注意**: `bot.cancel` 时将会让父 [Job] 也被 cancel.
-     * ```
-     * coroutineScope { // this: CoroutineScope
-     *   launch {
-     *     while(isActive) {
-     *       delay(500)
-     *       println("I'm alive")
-     *     }
-     *   }
-     *
-     *   val bot = Bot(...) {
-     *      inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
-     *   }
-     *   bot.login()
-     *   bot.cancel() // 取消了整个 `coroutineScope`, 因此上文不断打印 `"I'm alive"` 的协程也会被取消.
-     * }
-     * ```
-     *
-     * 因此, 此函数尤为适合在 `suspend fun main()` 中使用, 它能阻止主线程退出:
-     * ```
-     * suspend fun main() {
-     *   val bot = Bot() {
-     *     inheritCoroutineContext()
-     *   }
-     *   bot.eventChannel.subscribe { ... }
-     *
-     *   // 主线程不会退出, 直到 Bot 离线.
-     * }
-     * ```
-     *
-     * 简言之,
-     * - 若想让 [Bot] 作为 '守护进程' 运行, 则无需调用 [inheritCoroutineContext].
-     * - 若想让 [Bot] 依赖于当前协程, 让当前协程等待 [Bot] 运行, 则使用 [inheritCoroutineContext]
-     *
-     * @see parentCoroutineContext
-     */
-    @ConfigurationDsl
-    public actual suspend inline fun inheritCoroutineContext() {
-        parentCoroutineContext = coroutineContext
-    }
-
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Connection
-    ///////////////////////////////////////////////////////////////////////////
-
-    /** 连接心跳包周期. 过长会导致被服务器断开连接. */
-    public actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
-
-    /**
-     * 状态心跳包周期. 过长会导致掉线.
-     * 该值会在登录时根据服务器下发的配置自动进行更新.
-     * @since 2.6
-     * @see heartbeatStrategy
-     */
-    public actual var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
-
-    /**
-     * 心跳策略.
-     * @since 2.6.3
-     */
-    public actual var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
-
-    /**
-     * 心跳策略.
-     * @since 2.6.3
-     */
-    public actual enum class HeartbeatStrategy {
-        /**
-         * 使用 2.6.0 增加的*状态心跳* (Stat Heartbeat). 通常推荐这个模式.
-         *
-         * 该模式大多数情况下更稳定. 但有些账号使用这个模式时会遇到一段时间后发送消息成功但客户端不可见的问题.
-         */
-        STAT_HB,
-
-        /**
-         * 不发送状态心跳, 而是发送*切换在线状态* (可能会导致频繁的好友或客户端上线提示, 也可能产生短暂 (几秒) 发送消息不可见的问题).
-         *
-         * 建议在 [STAT_HB] 不可用时使用 [REGISTER].
-         */
-        REGISTER,
-
-        /**
-         * 不主动维护会话. 多数账号会每 16 分钟掉线然后重连. 则会有短暂的不可用时间.
-         *
-         * 仅当 [STAT_HB] 和 [REGISTER] 都造成无法接收等问题时使用.
-         * 同时请在 [https://github.com/mamoe/mirai/issues/1209] 提交问题.
-         */
-        NONE;
-    }
-
-    /**
-     * 每次心跳时等待结果的时间.
-     * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
-     */
-    public actual var heartbeatTimeoutMillis: Long = 5.secondsToMillis
-
-    /** 心跳失败后的第一次重连前的等待时间. */
-    @Deprecated(
-        "Useless since new network. Please just remove this.",
-        level = DeprecationLevel.HIDDEN
-    ) // deprecated since 2.7, error since 2.8
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
-
-    /** 重连失败后, 继续尝试的每次等待时间 */
-    @Deprecated(
-        "Useless since new network. Please just remove this.",
-        level = DeprecationLevel.HIDDEN
-    ) // deprecated since 2.7, error since 2.8
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
-    public actual var reconnectPeriodMillis: Long = 5.secondsToMillis
-
-    /** 最多尝试多少次重连 */
-    public actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
-
-    /**
-     * 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
-     *
-     * 其他情况掉线都默认会自动重连, 详见 [BotOfflineEvent.reconnect]
-     *
-     * @since 2.1
-     */
-    public actual var autoReconnectOnForceOffline: Boolean = false
-
-    /**
-     * 验证码处理器
-     *
-     * - 在 Android 需要手动提供 [LoginSolver]
-     * - 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
-     *
-     * 详见 [LoginSolver.Default]
-     *
-     * @see LoginSolver
-     */
-    public actual var loginSolver: LoginSolver? = LoginSolver.Default
-
-    /** 使用协议类型 */
-    public actual var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
-
-    public actual enum class MiraiProtocol {
-        /**
-         * Android 手机. 所有功能都支持.
-         */
-        ANDROID_PHONE,
-
-        /**
-         * Android 平板.
-         *
-         * 注意: 不支持戳一戳事件解析
-         */
-        ANDROID_PAD,
-
-        /**
-         * Android 手表.
-         */
-        ANDROID_WATCH,
-
-        /**
-         * iPad - 来自MiraiGo
-         *
-         * @since 2.8
-         */
-        IPAD,
-
-        /**
-         * MacOS - 来自MiraiGo
-         *
-         * @since 2.8
-         */
-        MACOS,
-
-    }
-
-    /**
-     * Highway 通道上传图片, 语音, 文件等资源时的协程数量.
-     *
-     * 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
-     * 默认: CPU 核心数.
-     *
-     * @since 2.2
-     */
-    public actual var highwayUploadCoroutineCount: Int = availableProcessors()
-
-    /**
-     * 设置 [autoReconnectOnForceOffline] 为 `true`, 即在被挤下线时自动重连.
-     * @since 2.1
-     */
-    @ConfigurationDsl
-    public actual fun autoReconnectOnForceOffline() {
-        autoReconnectOnForceOffline = true
-    }
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Device
-    ///////////////////////////////////////////////////////////////////////////
-
-    internal actual var accountSecrets: Boolean = true
-
-    /**
-     * 禁止保存 `account.secrets`.
-     *
-     * `account.secrets` 保存账号的会话信息。
-     * 它可加速登录过程,也可能可以减少出现验证码的次数。如果遇到一段时间后无法接收消息通知等同步问题时可尝试禁用。
-     *
-     * @since 2.11
-     */
-    public actual fun disableAccountSecretes() {
-        accountSecrets = false
-    }
-
-    /**
-     * 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
-     * @see fileBasedDeviceInfo 使用指定文件存储设备信息
-     * @see randomDeviceInfo 使用随机设备信息
-     */
-    public actual var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
-
-    /**
-     * 使用随机设备信息.
-     *
-     * @see deviceInfo
-     */
-    @ConfigurationDsl
-    public actual fun randomDeviceInfo() {
-        deviceInfo = null
-    }
-
-
-    /**
-     * 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
-     *
-     * @see deviceInfo
-     */
-    @ConfigurationDsl
-    public actual fun loadDeviceInfoJson(json: String) {
-        deviceInfo = {
-            DeviceInfoManager.deserialize(json, Companion.json)
-        }
-    }
-
     /**
      * 使用文件存储设备信息.
      *
@@ -325,80 +34,17 @@ public actual open class BotConfiguration { // open for Java
      * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
      * @see deviceInfo
      */
-    @ConfigurationDsl
+    @BotConfiguration.ConfigurationDsl
     public actual fun fileBasedDeviceInfo(filepath: String) {
         deviceInfo = {
             val file = MiraiFile.create(workingDir).resolve(filepath)
             if (!file.exists()) {
-                file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), json))
+                file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), BotConfiguration.json))
             }
-            DeviceInfoManager.deserialize(file.readText(), json)
+            DeviceInfoManager.deserialize(file.readText(), BotConfiguration.json)
         }
     }
 
-    ///////////////////////////////////////////////////////////////////////////
-    // Logging
-    ///////////////////////////////////////////////////////////////////////////
-
-    /**
-     * 日志记录器
-     *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
-     * - 忽略所有日志: [noBotLog]
-     * - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
-     * - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
-     *
-     * @see MiraiLogger
-     */
-    public actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
-    }
-
-    /**
-     * 网络层日志构造器
-     *
-     * - 默认打印到标准输出, 通过 [MiraiLogger.create]
-     * - 忽略所有日志: [noNetworkLog]
-     * - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
-     * - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
-     *
-     * @see MiraiLogger
-     */
-    public actual var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
-    }
-
-    /**
-     * 不显示网络日志. 不推荐.
-     * @see networkLoggerSupplier 更多日志处理方式
-     */
-    @ConfigurationDsl
-    public actual fun noNetworkLog() {
-        networkLoggerSupplier = { _ -> SilentLogger }
-    }
-
-    /**
-     * 不显示 [Bot] 日志. 不推荐.
-     * @see botLoggerSupplier 更多日志处理方式
-     */
-    @ConfigurationDsl
-    public actual fun noBotLog() {
-        botLoggerSupplier = { _ -> SilentLogger }
-    }
-
-    /**
-     * 是否显示过于冗长的事件日志
-     *
-     * 默认为 `false`
-     *
-     * @since 2.8
-     */
-    public actual var isShowingVerboseEventLog: Boolean = false
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Cache
-    //////////////////////////////////////////////////////////////////////////
-
     /**
      * 缓存数据目录路径. 若 [cacheDir] 为绝对路径, 将解析该绝对路径, 否则作为相对于 [workingDir] 的路径解析.
      * 例如, `cache` 将会解析为 `$workingDir/cache`, 而 `/Users/Chisato/Desktop/bot/cache` 指代绝对路径, 将解析为绝对路径.
@@ -417,147 +63,8 @@ public actual open class BotConfiguration { // open for Java
      */
     public var cacheDir: String = "cache"
 
-    /**
-     * 联系人信息缓存配置. 将会保存在 [cacheDir] 中 `contacts` 目录
-     * @since 2.4
-     */
-    public actual var contactListCache: ContactListCache = ContactListCache()
-
-    /**
-     * 联系人信息缓存配置
-     * @see contactListCache
-     * @see enableContactCache
-     * @see disableContactCache
-     * @since 2.4
-     */
-    public actual class ContactListCache {
-        /**
-         * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
-         */
-        public actual var saveIntervalMillis: Long = 60_000
-
-        /**
-         * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
-         */ // was @ExperimentalTime before 2.9
-        public actual inline var saveInterval: Duration
-            inline get() = saveIntervalMillis.milliseconds
-            inline set(v) {
-                saveIntervalMillis = v.inWholeMilliseconds
-            }
-
-        /**
-         * 开启好友列表缓存.
-         */
-        public actual var friendListCacheEnabled: Boolean = false
-
-        /**
-         * 开启群成员列表缓存.
-         */
-        public actual var groupMemberListCacheEnabled: Boolean = false
-    }
-
-    /**
-     * 配置 [ContactListCache]
-     * ```
-     * contactListCache {
-     *     saveIntervalMillis = 30_000
-     *     friendListCacheEnabled = true
-     * }
-     * ```
-     * @since 2.4
-     */
-    public actual inline fun contactListCache(action: ContactListCache.() -> Unit) {
-        action.invoke(this.contactListCache)
-    }
-
-    /**
-     * 禁用好友列表和群成员列表的缓存.
-     * @since 2.4
-     */
-    @ConfigurationDsl
-    public actual fun disableContactCache() {
-        contactListCache.friendListCacheEnabled = false
-        contactListCache.groupMemberListCacheEnabled = false
-    }
-
-    /**
-     * 启用好友列表和群成员列表的缓存.
-     * @since 2.4
-     */
-    @ConfigurationDsl
-    public actual fun enableContactCache() {
-        contactListCache.friendListCacheEnabled = true
-        contactListCache.groupMemberListCacheEnabled = true
-    }
-
-    /**
-     * 登录缓存.
-     *
-     * 开始后在密码登录成功时会保存秘钥等信息, 在下次启动时通过这些信息登录, 而不提交密码.
-     * 可以减少验证码出现的频率.
-     *
-     * 秘钥信息会由密码加密保存. 如果秘钥过期, 则会进行普通密码登录.
-     *
-     * 默认 `true` (开启).
-     *
-     * @since 2.6
-     */
-    public actual var loginCacheEnabled: Boolean = true
-
-    ///////////////////////////////////////////////////////////////////////////
-    // Misc
-    ///////////////////////////////////////////////////////////////////////////
-
-    @Suppress("DuplicatedCode")
-    public actual fun copy(): BotConfiguration {
-        return BotConfiguration().also { new ->
-            // To structural order
-            new.workingDir = workingDir
-            new.parentCoroutineContext = parentCoroutineContext
-            new.heartbeatPeriodMillis = heartbeatPeriodMillis
-            new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
-            new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
-            new.heartbeatStrategy = heartbeatStrategy
-            new.reconnectionRetryTimes = reconnectionRetryTimes
-            new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
-            new.loginSolver = loginSolver
-            new.protocol = protocol
-            new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
-            new.accountSecrets = accountSecrets
-            new.deviceInfo = deviceInfo
-            new.botLoggerSupplier = botLoggerSupplier
-            new.networkLoggerSupplier = networkLoggerSupplier
-            new.cacheDir = cacheDir
-            new.contactListCache = contactListCache
-            new.convertLineSeparator = convertLineSeparator
-            new.isShowingVerboseEventLog = isShowingVerboseEventLog
-        }
-    }
-
-    /**
-     * 是否处理接受到的特殊换行符, 默认为 `true`
-     *
-     * - 若为 `true`, 会将收到的 `CRLF(\r\n)` 和 `CR(\r)` 替换为 `LF(\n)`
-     * - 若为 `false`, 则不做处理
-     *
-     * @since 2.4
-     */
-    public actual var convertLineSeparator: Boolean = true
-
-    /** 标注一个配置 DSL 函数 */
-    @Target(AnnotationTarget.FUNCTION)
-    @DslMarker
-    public actual annotation class ConfigurationDsl
-
-    public actual companion object {
-        /** 默认的配置实例. 可以进行修改 */
-        public actual val Default: BotConfiguration = BotConfiguration()
-
-
-        private val json = Json {
-            isLenient = true
-            ignoreUnknownKeys = true
-            prettyPrint = true
-        }
+    internal actual fun applyMppCopy(new: BotConfiguration) {
+        new.workingDir = workingDir
+        new.cacheDir = cacheDir
     }
 }

+ 2 - 2
mirai-core-mock/build.gradle.kts

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -30,7 +30,7 @@ dependencies {
     implementation(`ktor-server-core`)
     implementation(`ktor-server-netty`)
     implementation(`java-in-memory-file-system`)
-
+    implementation(`kotlin-jvm-blocking-bridge`)
 }
 
 tasks.register("buildRuntimeClasspath") { // this task is used for mirai-mock-framework (external)

+ 9 - 4
mirai-core-mock/src/contact/MockGroup.kt

@@ -17,7 +17,6 @@ import net.mamoe.mirai.contact.NormalMember
 import net.mamoe.mirai.data.GroupHonorType
 import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.broadcast
-import net.mamoe.mirai.event.events.MemberHonorChangeEvent
 import net.mamoe.mirai.event.events.MemberJoinRequestEvent
 import net.mamoe.mirai.mock.MockBot
 import net.mamoe.mirai.mock.MockBotDSL
@@ -51,6 +50,11 @@ public interface MockGroup : Group, MockContact, MockMsgSyncSupport {
      * @see changeHonorMember
      */
     @MockBotDSL
+    @Deprecated(
+        "use active.changeHonorMember",
+        ReplaceWith(".active.changeHonorMember(member, honorType)"),
+        level = DeprecationLevel.ERROR
+    )
     public val honorMembers: MutableMap<GroupHonorType, MockNormalMember>
 
     /**
@@ -61,10 +65,11 @@ public interface MockGroup : Group, MockContact, MockMsgSyncSupport {
      * 此外如果 [honorType] 是 [GroupHonorType.TALKATIVE],
      * 会额外广播 [net.mamoe.mirai.event.events.GroupTalkativeChangeEvent].
      *
-     * 如果不需要广播事件, 可直接更改 [MockGroup.honorMembers]
+     * 如果不需要广播事件, 可直接使用 [MockGroupActive.mockSetHonorHistory]
      */
-    @MockBotDSL
-    public fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType)
+    public fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType) {
+        active.changeHonorMember(member, honorType)
+    }
 
     /**
      * 获取群控制面板

+ 16 - 0
mirai-core-mock/src/contact/active/MockGroupActive.kt

@@ -11,6 +11,9 @@ package net.mamoe.mirai.mock.contact.active
 
 import net.mamoe.mirai.contact.active.*
 import net.mamoe.mirai.data.GroupHonorType
+import net.mamoe.mirai.event.events.MemberHonorChangeEvent
+import net.mamoe.mirai.mock.MockBotDSL
+import net.mamoe.mirai.mock.contact.MockNormalMember
 
 public interface MockGroupActive : GroupActive {
     /**
@@ -39,4 +42,17 @@ public interface MockGroupActive : GroupActive {
      * @see queryActiveRank
      */
     public fun mockSetRankRecords(list: List<ActiveRankRecord>)
+
+    /**
+     * 更改拥有群荣耀的群成员.
+     *
+     * 会自动广播 [MemberHonorChangeEvent.Achieve] 和 [MemberHonorChangeEvent.Lose] 等相关事件.
+     *
+     * 此外如果 [honorType] 是 [GroupHonorType.TALKATIVE],
+     * 会额外广播 [net.mamoe.mirai.event.events.GroupTalkativeChangeEvent].
+     *
+     * 如果不需要广播事件, 可直接使用 [mockSetHonorHistory]
+     */
+    @MockBotDSL
+    public fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType)
 }

+ 24 - 0
mirai-core-mock/src/contact/essence/MockEssences.kt

@@ -0,0 +1,24 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.contact.essence
+
+import net.mamoe.mirai.contact.NormalMember
+import net.mamoe.mirai.contact.essence.Essences
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.mock.MockBotDSL
+
+public interface MockEssences : Essences {
+
+    /**
+     * 直接以 [actor] 的身份设置一条精华消息
+     */
+    @MockBotDSL
+    public fun mockSetEssences(source: MessageSource, actor: NormalMember)
+}

+ 8 - 0
mirai-core-mock/src/internal/MockBotFactoryImpl.kt

@@ -10,6 +10,7 @@
 package net.mamoe.mirai.mock.internal
 
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.auth.BotAuthorization
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.mock.MockBot
 import net.mamoe.mirai.mock.MockBotFactory
@@ -115,4 +116,11 @@ internal class MockBotFactoryImpl : MockBotFactory {
             .configuration(configuration)
             .create()
     }
+
+    override fun newBot(qq: Long, authorization: BotAuthorization, configuration: BotConfiguration): Bot {
+        return newMockBotBuilder()
+            .id(qq)
+            .configuration(configuration)
+            .create()
+    }
 }

+ 2 - 4
mirai-core-mock/src/internal/MockMiraiImpl.kt

@@ -29,10 +29,7 @@ import net.mamoe.mirai.mock.MockBotFactory
 import net.mamoe.mirai.mock.contact.MockGroup
 import net.mamoe.mirai.mock.database.queryMessageInfo
 import net.mamoe.mirai.mock.database.removeMessageInfo
-import net.mamoe.mirai.mock.internal.contact.AQQ_RECALL_FAILED_MESSAGE
-import net.mamoe.mirai.mock.internal.contact.MockFriendImpl
-import net.mamoe.mirai.mock.internal.contact.MockImage
-import net.mamoe.mirai.mock.internal.contact.MockStrangerImpl
+import net.mamoe.mirai.mock.internal.contact.*
 import net.mamoe.mirai.mock.internal.msgsrc.registerMockMsgSerializers
 import net.mamoe.mirai.mock.utils.mock
 import net.mamoe.mirai.mock.utils.simpleMemberInfo
@@ -42,6 +39,7 @@ internal class MockMiraiImpl : MiraiImpl() {
     companion object {
         init {
             registerMockMsgSerializers()
+            registerMockServices()
         }
     }
 

+ 4 - 3
mirai-core-mock/src/internal/contact/MockAnnouncementsImpl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -55,9 +55,10 @@ internal class MockAnnouncementsImpl(
     }
 
     override fun mockPublish(announcement: Announcement, actor: NormalMember, events: Boolean): OnlineAnnouncement {
-        val old = if (announcement.parameters.sendToNewMember)
+        if (announcement.parameters.sendToNewMember) {
             announcements.elements().toList().firstOrNull { oa -> oa.parameters.sendToNewMember }
-        else null
+        }
+
         val ann = MockOnlineAnnouncement(
             content = announcement.content,
             parameters = announcement.parameters,

+ 1 - 1
mirai-core-mock/src/internal/contact/MockFriendImpl.kt

@@ -124,7 +124,7 @@ internal class MockFriendImpl(
 
     override suspend fun says(message: MessageChain): MessageChain {
         val src = newMsgSrc(true, message) { ids, internalIds, time ->
-            OnlineMsgSrcFromFriend(ids, internalIds, time, message, bot, this)
+            OnlineMsgSrcFromFriend(ids, internalIds, time, message, bot, this, bot)
         }
         val msg = src.withMessage(message)
         FriendMessageEvent(this, msg, src.time).broadcast()

+ 11 - 11
mirai-core-mock/src/internal/contact/MockGroupImpl.kt

@@ -31,7 +31,9 @@ import net.mamoe.mirai.mock.contact.MockGroup
 import net.mamoe.mirai.mock.contact.MockGroupControlPane
 import net.mamoe.mirai.mock.contact.MockNormalMember
 import net.mamoe.mirai.mock.contact.active.MockGroupActive
+import net.mamoe.mirai.mock.contact.essence.MockEssences
 import net.mamoe.mirai.mock.internal.contact.active.MockGroupActiveImpl
+import net.mamoe.mirai.mock.internal.contact.essence.MockEssencesImpl
 import net.mamoe.mirai.mock.internal.contact.roaming.MockRoamingMessages
 import net.mamoe.mirai.mock.internal.msgsrc.OnlineMsgSrcToGroup
 import net.mamoe.mirai.mock.internal.msgsrc.newMsgSrc
@@ -50,6 +52,11 @@ internal class MockGroupImpl(
 ) : AbstractMockContact(
     parentCoroutineContext, bot, id
 ), MockGroup {
+    @Deprecated(
+        "use active.changeHonorMember",
+        replaceWith = ReplaceWith(".active.changeHonorMember(member, honorType)"),
+        level = DeprecationLevel.ERROR
+    )
     override val honorMembers: MutableMap<GroupHonorType, MockNormalMember> = ConcurrentHashMap()
     private val txFileSystem by lazy { bot.mock().tmpResourceServer.mockServerFileDisk.newFsSystem() }
 
@@ -59,17 +66,6 @@ internal class MockGroupImpl(
 
     override val active: MockGroupActive by lazy { MockGroupActiveImpl(this) }
 
-    override fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType) {
-        val onm = honorMembers[honorType]
-        honorMembers[honorType] = member
-        // reference net.mamoe.mirai.internal.network.notice.group.NoticePipelineContext.processGeneralGrayTip, GroupNotificationProcessor.kt#361L
-        if (honorType == GroupHonorType.TALKATIVE) {
-            if (onm != null) GroupTalkativeChangeEvent(this, member, onm).broadcastBlocking()
-        }
-        if (onm != null) MemberHonorChangeEvent.Lose(onm, honorType).broadcastBlocking()
-        MemberHonorChangeEvent.Achieve(member, honorType).broadcastBlocking()
-    }
-
     override fun appendMember(mockMember: MemberInfo): MockGroup {
         addMember(mockMember)
         return this
@@ -337,9 +333,13 @@ internal class MockGroupImpl(
         resource.mockUploadVoice(bot)
 
     override suspend fun setEssenceMessage(source: MessageSource): Boolean {
+        checkBotPermission(MemberPermission.ADMINISTRATOR)
+        essences.mockSetEssences(source, this.botAsMember)
         return true
     }
 
+    override val essences: MockEssences = MockEssencesImpl(this)
+
     @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"))
     @Suppress("OverridingDeprecatedMember", "DEPRECATION", "DEPRECATION_ERROR")
     override val filesRoot: RemoteFile by lazy {

+ 1 - 1
mirai-core-mock/src/internal/contact/MockStrangerImpl.kt

@@ -87,7 +87,7 @@ internal class MockStrangerImpl(
 
     override suspend fun says(message: MessageChain): MessageChain {
         val src = newMsgSrc(true, message) { ids, internalIds, time ->
-            OnlineMsgSrcFromStranger(ids, internalIds, time, message, bot, this)
+            OnlineMsgSrcFromStranger(ids, internalIds, time, message, bot, this, bot)
         }
         val msg = src.withMessage(message)
         StrangerMessageEvent(this, msg, src.time).broadcast()

+ 32 - 4
mirai-core-mock/src/internal/contact/active/MockGroupActive.kt

@@ -12,14 +12,15 @@ package net.mamoe.mirai.mock.internal.contact.active
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.asFlow
 import net.mamoe.mirai.contact.MemberPermission
-import net.mamoe.mirai.contact.active.ActiveChart
-import net.mamoe.mirai.contact.active.ActiveHonorList
-import net.mamoe.mirai.contact.active.ActiveRankRecord
-import net.mamoe.mirai.contact.active.ActiveRecord
+import net.mamoe.mirai.contact.active.*
 import net.mamoe.mirai.contact.checkBotPermission
 import net.mamoe.mirai.data.GroupHonorType
+import net.mamoe.mirai.event.events.GroupTalkativeChangeEvent
+import net.mamoe.mirai.event.events.MemberHonorChangeEvent
+import net.mamoe.mirai.mock.contact.MockNormalMember
 import net.mamoe.mirai.mock.contact.active.MockGroupActive
 import net.mamoe.mirai.mock.internal.contact.MockGroupImpl
+import net.mamoe.mirai.mock.utils.broadcastBlocking
 import net.mamoe.mirai.utils.ConcurrentHashMap
 import net.mamoe.mirai.utils.JavaFriendlyAPI
 import net.mamoe.mirai.utils.asImmutable
@@ -82,6 +83,33 @@ internal class MockGroupActiveImpl(
 
     private var honorHistories: MutableMap<GroupHonorType, ActiveHonorList> = ConcurrentHashMap()
 
+    @Suppress("INVISIBLE_MEMBER") // for ActiveHonorInfo
+    override fun changeHonorMember(member: MockNormalMember, honorType: GroupHonorType) {
+        val old = honorHistories[honorType]
+
+        val info = ActiveHonorInfo(member.nameCard, member.id, member.avatarUrl, member, 0, 0, 0)
+        if (old == null) {
+            // if not history record found, add a new one with current honor member
+            honorHistories[honorType] = ActiveHonorList(honorType, info, emptyList())
+        } else if (old.current?.memberId != info.memberId) {
+            honorHistories[honorType] =
+                ActiveHonorList(honorType, info, old.current?.let {
+                    old.records.plus(it)
+                } ?: old.records)
+            if (old.current != null) {
+                if (honorType == GroupHonorType.TALKATIVE) {
+                    GroupTalkativeChangeEvent(
+                        this.group,
+                        member,
+                        old.current!!.member!!
+                    ).broadcastBlocking()
+                }
+                MemberHonorChangeEvent.Lose(old.current!!.member!!, honorType).broadcastBlocking()
+            }
+        }
+        MemberHonorChangeEvent.Achieve(member, honorType).broadcastBlocking()
+    }
+
     override suspend fun queryHonorHistory(type: GroupHonorType): ActiveHonorList {
         return honorHistories.getOrElse(type) { ActiveHonorList(type, null, listOf()) }
     }

+ 59 - 0
mirai-core-mock/src/internal/contact/essence/MockEssences.kt

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.mock.internal.contact.essence
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import net.mamoe.mirai.contact.NormalMember
+import net.mamoe.mirai.contact.essence.EssenceMessageRecord
+import net.mamoe.mirai.message.data.MessageSource
+import net.mamoe.mirai.mock.contact.essence.MockEssences
+import net.mamoe.mirai.mock.internal.contact.MockGroupImpl
+import net.mamoe.mirai.utils.ConcurrentHashMap
+import net.mamoe.mirai.utils.currentTimeSeconds
+
+internal class MockEssencesImpl(
+    private val group: MockGroupImpl
+) : MockEssences {
+
+    private val cache: MutableMap<MessageSource, EssenceMessageRecord> = ConcurrentHashMap()
+
+    override fun mockSetEssences(source: MessageSource, actor: NormalMember) {
+        val record = EssenceMessageRecord(
+            group = group,
+            sender = group[source.fromId],
+            senderId = source.fromId,
+            senderNick = group[source.fromId]?.nick.orEmpty(),
+            senderTime = source.time,
+            operator = actor,
+            operatorId = actor.id,
+            operatorNick = actor.nick,
+            operatorTime = currentTimeSeconds().toInt(),
+            loadMessageSource = { source }
+        )
+        cache[source] = record
+    }
+
+    override suspend fun getPage(start: Int, limit: Int): List<EssenceMessageRecord> {
+        return cache.values.toList().subList(start, start + limit)
+    }
+
+    override suspend fun share(source: MessageSource): String {
+        return "https://qun.qq.com/essence/share?_wv=3&_wwv=128&_wvx=2&sharekey=..."
+    }
+
+    override suspend fun remove(source: MessageSource) {
+        cache.remove(source)
+    }
+
+    override fun asFlow(): Flow<EssenceMessageRecord> {
+        return cache.values.asFlow()
+    }
+}

+ 46 - 9
mirai-core-mock/src/internal/contact/util.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -13,6 +13,7 @@ package net.mamoe.mirai.mock.internal.contact
 
 import kotlinx.serialization.Serializable
 import net.mamoe.mirai.Bot
+import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.contact.PermissionDeniedException
@@ -26,6 +27,7 @@ import net.mamoe.mirai.mock.utils.mock
 import net.mamoe.mirai.mock.utils.plusHttpSubpath
 import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.Services
 import net.mamoe.mirai.utils.cast
 import net.mamoe.mirai.utils.toUHexString
 
@@ -50,14 +52,17 @@ internal fun MessageSource.withMessage(msg: Message): MessageChain = buildMessag
 }
 
 @Suppress("UNUSED_PARAMETER")
-internal suspend fun ExternalResource.mockUploadAudio(bot: MockBot) = inResource {
-    OfflineAudio(
-        filename = md5.toUHexString() + ".amr",
-        fileMd5 = md5,
-        fileSize = size,
-        codec = AudioCodec.SILK,
-        extraData = null,
-    )
+internal suspend fun ExternalResource.mockUploadAudio(bot: MockBot): OfflineAudio {
+    val md5 = md5 // calculate before using resource
+    return inResource {
+        OfflineAudio(
+            filename = md5.toUHexString() + ".amr",
+            fileMd5 = md5,
+            fileSize = size,
+            codec = AudioCodec.SILK,
+            extraData = null,
+        )
+    }
 }
 
 internal suspend fun ExternalResource.mockUploadVoice(bot: MockBot) = kotlin.run {
@@ -152,3 +157,35 @@ internal class MockImage(
         return this.imageId == other.imageId
     }
 }
+
+internal object MockInternalImageProtocolImpl : InternalImageProtocol {
+
+    override fun createImage(
+        imageId: String,
+        size: Long,
+        type: ImageType,
+        width: Int,
+        height: Int,
+        isEmoji: Boolean
+    ): Image = MockImage(imageId, "images/" + imageId.substring(1..36), width, height, size, type)
+
+    override suspend fun isUploaded(
+        bot: Bot,
+        md5: ByteArray,
+        size: Long,
+        context: Contact?,
+        type: ImageType,
+        width: Int,
+        height: Int
+    ): Boolean = bot.cast<MockBot>().tmpResourceServer.isImageUploaded(md5, size)
+
+}
+
+internal fun registerMockServices() {
+    Services.registerAsOverride(
+        Services.qualifiedNameOrFail(InternalImageProtocol::class),
+        "net.mamoe.mirai.mock.internal.contact.MockInternalImageProtocolImpl"
+    ) {
+        MockInternalImageProtocolImpl
+    }
+}

+ 24 - 3
mirai-core-mock/src/internal/msgsrc/OnlineMsgSrc.kt

@@ -23,6 +23,7 @@ import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.mock.internal.contact.AbstractMockContact
 import net.mamoe.mirai.mock.internal.contact.MockImage
+import net.mamoe.mirai.utils.cast
 import net.mamoe.mirai.utils.currentTimeSeconds
 
 internal fun registerMockMsgSerializers() {
@@ -174,10 +175,17 @@ internal class OnlineMsgSrcFromFriend(
     override val time: Int,
     override val originalMessage: MessageChain,
     override val bot: Bot,
-    override val sender: Friend
+    override val sender: Friend,
+    override val target: ContactOrBot,
 ) : OnlineMessageSource.Incoming.FromFriend() {
     override val isOriginalMessageInitialized: Boolean get() = true
 
+    override val subject: Friend
+        get() {
+            if (target is Bot) return sender
+            return target.cast()
+        }
+
     object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("Mock_OnlineMessageSourceFromFriend")
 }
 
@@ -189,10 +197,17 @@ internal class OnlineMsgSrcFromStranger(
     override val time: Int,
     override val originalMessage: MessageChain,
     override val bot: Bot,
-    override val sender: Stranger
+    override val sender: Stranger,
+    override val target: ContactOrBot,
 ) : OnlineMessageSource.Incoming.FromStranger() {
     override val isOriginalMessageInitialized: Boolean get() = true
 
+    override val subject: Stranger
+        get() {
+            if (target is Bot) return sender
+            return target.cast()
+        }
+
     object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl(
         "Mock_OnlineMessageSourceFromStranger"
     )
@@ -206,9 +221,15 @@ internal class OnlineMsgSrcFromTemp(
     override val time: Int,
     override val originalMessage: MessageChain,
     override val bot: Bot,
-    override val sender: Member
+    override val sender: Member,
+    override val target: ContactOrBot,
 ) : OnlineMessageSource.Incoming.FromTemp() {
     override val isOriginalMessageInitialized: Boolean get() = true
+    override val subject: Member
+        get() {
+            if (target is Bot) return sender
+            return target.cast()
+        }
 
     object Serializer : KSerializer<MessageSource> by MessageSourceSerializerImpl("Mock_OnlineMessageSourceFromTemp")
 

+ 6 - 4
mirai-core-mock/src/internal/remotefile/absolutefile/MockAbsoluteFolder.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -163,9 +163,11 @@ internal open class MockAbsoluteFolder(
         if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.")
         if (!FileSystem.isLegal(path)) return emptyFlow()
         if (path[0] == '/') return files.root.resolveFiles(path.removePrefix("/"))
-        return files.fileSystem.findByPath(absolutePath.removeSuffix("/") + "/" + path.removePrefix("/")).map {
-            it.toMockAbsFile(files)
-        }.asFlow()
+        return files.fileSystem.findByPath(absolutePath.removeSuffix("/") + "/" + path.removePrefix("/"))
+            .filter { it.isFile }
+            .map {
+                it.toMockAbsFile(files)
+            }.asFlow()
     }
 
     @JavaFriendlyAPI

+ 2 - 2
mirai-core-mock/src/internal/serverfs/MockServerFileDiskImpl.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -249,7 +249,7 @@ internal class MockServerFileImpl(
         system.resolvePath(details.resolve("parent").readText())
             .resolve(id.substring(1))
             .deleteIfExists()
-        details.deleteRecursively()
+        details.deleteRecursivelyMirai()
         return true
     }
 

+ 10 - 0
mirai-core-mock/src/internal/serverfs/TmpResourceServerImpl.kt

@@ -21,6 +21,7 @@ import java.net.ServerSocket
 import java.net.URI
 import java.net.URLDecoder
 import java.net.URLEncoder
+import java.nio.file.Files
 import java.nio.file.Path
 import kotlin.io.path.*
 
@@ -65,6 +66,15 @@ internal class TmpResourceServerImpl(
         }
     }
 
+    override fun isImageUploaded(md5: ByteArray, size: Long): Boolean {
+        val img = images.resolve(generateUUID(md5))
+        if (img.exists()) {
+            return Files.size(img) == size
+        }
+        return false
+    }
+
+
     override suspend fun uploadResourceAsImage(resource: ExternalResource): URI {
         val imgId = generateUUID(resource.md5)
         val resId = uploadResource(resource)

+ 5 - 0
mirai-core-mock/src/resserver/TmpResourceServer.kt

@@ -47,6 +47,11 @@ public interface TmpResourceServer : Closeable {
      * @return 图片的 http 链接
      */
     public suspend fun uploadResourceAsImage(resource: ExternalResource): URI
+
+    /**
+     * 通过图片 md5 和 size 判断图片是否已经上传
+     */
+    public fun isImageUploaded(md5: ByteArray, size: Long): Boolean
     public suspend fun uploadResourceAndGetUrl(resource: ExternalResource): String {
         return resolveHttpUrl(uploadResource(resource)).toString()
     }

+ 34 - 1
mirai-core-mock/test/AbsoluteFileTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -11,18 +11,21 @@ package net.mamoe.mirai.mock.test
 
 import com.google.common.jimfs.Configuration
 import com.google.common.jimfs.Jimfs
+import kotlinx.coroutines.flow.count
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.toList
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.event.events.GroupMessageEvent
 import net.mamoe.mirai.message.data.FileMessage
+import net.mamoe.mirai.mock.internal.contact.mockUploadAudio
 import net.mamoe.mirai.mock.internal.remotefile.absolutefile.MockRemoteFiles
 import net.mamoe.mirai.mock.internal.serverfs.MockServerFileSystemImpl
 import net.mamoe.mirai.mock.utils.simpleMemberInfo
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
 import net.mamoe.mirai.utils.cast
 import net.mamoe.mirai.utils.md5
+import net.mamoe.mirai.utils.runBIO
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Test
 import java.nio.file.FileSystem
@@ -129,4 +132,34 @@ internal class AbsoluteFileTest : MockBotTestBase() {
         val file = files.root.resolveFileById(absFile.id, true)!!
         assertContentEquals(bytes.md5(), file.md5)
     }
+
+    @Test
+    fun testResolveFiles() = runTest {
+        val file = runBIO {
+            kotlin.io.path.createTempFile("test", ".txt").toFile().apply {
+                writeText("test")
+                deleteOnExit()
+            }
+        }
+        file.toExternalResource().use {
+            group.files.root.uploadNewFile("/a/test.txt", it)
+        }
+        assertEquals(0, group.files.root.resolveFiles("/a").count())
+    }
+
+    @Test
+    @Suppress("INVISIBLE_REFERENCE")
+    fun testMockUploadAudio() = runTest {
+        val file = runBIO {
+            kotlin.io.path.createTempFile("test", ".txt").toFile().apply {
+                writeText("test")
+                deleteOnExit()
+            }
+        }
+
+        file.toExternalResource().use {
+            assertIsInstance<net.mamoe.mirai.internal.utils.ExternalResourceImplByFile>(it)
+            it.mockUploadAudio(bot)
+        }
+    }
 }

+ 21 - 0
mirai-core-mock/test/ImageUploadTest.kt

@@ -11,15 +11,18 @@ package net.mamoe.mirai.mock.test
 
 import kotlinx.coroutines.runBlocking
 import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.Image.Key.isUploaded
 import net.mamoe.mirai.message.data.Image.Key.queryUrl
 import net.mamoe.mirai.mock.MockBotFactory
 import net.mamoe.mirai.mock.utils.randomImageContent
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.getRandomByteArray
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.TestInstance
 import java.net.URL
 import kotlin.test.assertEquals
+import kotlin.test.assertFalse
 import kotlin.test.assertNotEquals
 import kotlin.test.assertTrue
 
@@ -56,4 +59,22 @@ internal class ImageUploadTest {
             assertEquals(img1, img2)
         }
     }
+
+    @Test
+    fun testImageIsUploaded(): Unit = runBlocking {
+        val img = Image.randomImageContent().toExternalResource().use { imgData ->
+            bot.asFriend.uploadImage(imgData)
+        }
+        assertTrue { img.isUploaded(bot) }
+    }
+
+    @Test
+    @Suppress("RemoveRedundantQualifierName")
+    fun testImageIsUploadedNotTrue(): Unit = runBlocking {
+        assertFalse { Image.isUploaded(bot, getRandomByteArray(16), 10) }
+        val img = Image.randomImageContent().toExternalResource().use { imgData ->
+            bot.asFriend.uploadImage(imgData)
+        }
+        assertFalse { Image.isUploaded(bot, img.md5, img.size + 5) }
+    }
 }

+ 2 - 2
mirai-core-mock/test/mock/MessageSerializationTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -109,7 +109,7 @@ internal class MessageSerializationTest : MockBotTestBase() {
 
     @Test
     fun testSerializersModulePlus() {
-        MessageSerializers.serializersModule + EmptySerializersModule
+        MessageSerializers.serializersModule + EmptySerializersModule()
     }
 
     @Test

+ 2 - 2
mirai-core-mock/test/mock/MessagingTest.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -312,7 +312,7 @@ internal class MessagingTest : MockBotTestBase() {
             dynamicContainer("Normal messaging test") {
 
                 val group = bot.addGroup(18451444229, "owner group")
-                val owner = group.addMember(MockMemberInfoBuilder.create {
+                group.addMember(MockMemberInfoBuilder.create {
                     uin(184554).permission(MemberPermission.OWNER)
                 })
                 val administrator = group.addMember(MockMemberInfoBuilder.create {

+ 104 - 19
mirai-core-mock/test/mock/MockGroupTest.kt

@@ -163,25 +163,6 @@ internal class MockGroupTest : MockBotTestBase() {
         assertEquals(0, fsroot.listFilesCollection().size)
     }
 
-    @Test
-    internal fun testMemberHonorChangeEvent() = runTest {
-        runAndReceiveEventBroadcast {
-            val group = bot.addGroup(111, "aa")
-            val member1 = group.addMember(simpleMemberInfo(222, "bb", permission = MemberPermission.MEMBER))
-            val member2 = group.addMember(simpleMemberInfo(333, "cc", permission = MemberPermission.MEMBER))
-            group.honorMembers[GroupHonorType.TALKATIVE] = member1
-            group.changeHonorMember(member2, GroupHonorType.TALKATIVE)
-        }.let { events ->
-            assertEquals(3, events.size)
-            assertIsInstance<GroupTalkativeChangeEvent>(events[0])
-            assertIsInstance<MemberHonorChangeEvent.Lose>(events[1])
-            assertEquals(222, events[1].cast<MemberHonorChangeEvent.Lose>().member.id)
-            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[2])
-            assertEquals(GroupHonorType.TALKATIVE, events[2].cast<MemberHonorChangeEvent.Achieve>().honorType)
-            assertEquals(333, events[2].cast<MemberHonorChangeEvent.Achieve>().member.id)
-        }
-    }
-
     @Test
     internal fun testGroupFileUpload() = runTest {
         val files = bot.addGroup(111, "aaa").files
@@ -457,4 +438,108 @@ internal class MockGroupTest : MockBotTestBase() {
             }
         }
     }
+
+    @Test
+    fun testHonorMember() = runTest {
+        val group = bot.addGroup(1, "")
+        val member1 = group.addMember(2, "")
+        val member2 = group.addMember(3, "")
+        assertEquals(emptyList(), group.active.queryHonorHistory(GroupHonorType.TALKATIVE).records)
+
+        runAndReceiveEventBroadcast {
+            group.active.changeHonorMember(member1, GroupHonorType.TALKATIVE)
+        }.let { events ->
+            assertEquals(1, events.size)
+            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[0]) {
+                assertEquals(GroupHonorType.TALKATIVE, this.honorType)
+                assertEquals(member1, this.member)
+                assertEquals(group, this.group)
+            }
+        }
+        assertEquals(member1, group.active.queryHonorHistory(GroupHonorType.TALKATIVE).current!!.member!!)
+        assertEquals(emptyList(), group.active.queryHonorHistory(GroupHonorType.TALKATIVE).records)
+
+        runAndReceiveEventBroadcast {
+            group.active.changeHonorMember(member2, GroupHonorType.TALKATIVE)
+        }.let { events ->
+            assertEquals(3, events.size)
+            assertIsInstance<GroupTalkativeChangeEvent>(events[0]) {
+                assertEquals(member2, this.now)
+                assertEquals(member1, this.previous)
+                assertEquals(group, this.group)
+            }
+            assertIsInstance<MemberHonorChangeEvent.Lose>(events[1]) {
+                assertEquals(GroupHonorType.TALKATIVE, this.honorType)
+                assertEquals(member1, this.member)
+                assertEquals(group, this.group)
+            }
+            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[2]) {
+                assertEquals(GroupHonorType.TALKATIVE, this.honorType)
+                assertEquals(member2, this.member)
+                assertEquals(group, this.group)
+            }
+        }
+
+        assertEquals(member2, group.active.queryHonorHistory(GroupHonorType.TALKATIVE).current!!.member!!)
+        // it.member must exist
+        assertEquals(
+            listOf(member1),
+            group.active.queryHonorHistory(GroupHonorType.TALKATIVE).records.map { it.member!! })
+
+        runAndReceiveEventBroadcast {
+            group.active.changeHonorMember(member1, GroupHonorType.TALKATIVE)
+        }.let { events ->
+            assertEquals(3, events.size)
+            assertIsInstance<GroupTalkativeChangeEvent>(events[0]) {
+                assertEquals(member1, this.now)
+                assertEquals(member2, this.previous)
+                assertEquals(group, this.group)
+            }
+            assertIsInstance<MemberHonorChangeEvent.Lose>(events[1]) {
+                assertEquals(GroupHonorType.TALKATIVE, this.honorType)
+                assertEquals(member2, this.member)
+                assertEquals(group, this.group)
+            }
+            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[2]) {
+                assertEquals(GroupHonorType.TALKATIVE, this.honorType)
+                assertEquals(member1, this.member)
+                assertEquals(group, this.group)
+            }
+        }
+        assertEquals(member1, group.active.queryHonorHistory(GroupHonorType.TALKATIVE).current!!.member!!)
+        assertEquals(
+            listOf(member1, member2),
+            group.active.queryHonorHistory(GroupHonorType.TALKATIVE).records.map { it.member!! })
+
+        runAndReceiveEventBroadcast {
+            group.active.changeHonorMember(member1, GroupHonorType.BRONZE)
+        }.let { events ->
+            assertEquals(1, events.size)
+            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[0]) {
+                assertEquals(GroupHonorType.BRONZE, this.honorType)
+                assertEquals(member1, this.member)
+                assertEquals(group, this.group)
+            }
+        }
+        assertEquals(member1, group.active.queryHonorHistory(GroupHonorType.BRONZE).current!!.member!!)
+        assertEquals(emptyList(), group.active.queryHonorHistory(GroupHonorType.BRONZE).records)
+
+        runAndReceiveEventBroadcast {
+            group.active.changeHonorMember(member2, GroupHonorType.BRONZE)
+        }.let { events ->
+            assertEquals(2, events.size)
+            assertIsInstance<MemberHonorChangeEvent.Lose>(events[0]) {
+                assertEquals(GroupHonorType.BRONZE, this.honorType)
+                assertEquals(member1, this.member)
+                assertEquals(group, this.group)
+            }
+            assertIsInstance<MemberHonorChangeEvent.Achieve>(events[1]) {
+                assertEquals(GroupHonorType.BRONZE, this.honorType)
+                assertEquals(member2, this.member)
+                assertEquals(group, this.group)
+            }
+        }
+        assertEquals(member2, group.active.queryHonorHistory(GroupHonorType.BRONZE).current!!.member!!)
+        assertEquals(listOf(member1), group.active.queryHonorHistory(GroupHonorType.BRONZE).records.map { it.member!! })
+    }
 }

+ 0 - 76
mirai-core-utils/src/commonMain/kotlin/IO.kt

@@ -75,82 +75,6 @@ public inline fun ByteReadPacket.readPacketExact(
 ): ByteReadPacket = this.readBytes(n).toReadPacket()
 
 
-public typealias TlvMap = MutableMap<Int, ByteArray>
-
-public inline fun TlvMap.getOrFail(tag: Int): ByteArray {
-    return this[tag] ?: error("cannot find tlv 0x${tag.toUHexString("")}($tag)")
-}
-
-public inline fun TlvMap.getOrFail(tag: Int, lazyMessage: (tag: Int) -> String): ByteArray {
-    return this[tag] ?: error(lazyMessage(tag))
-}
-
-@Suppress("FunctionName")
-public inline fun Input._readTLVMap(tagSize: Int = 2, suppressDuplication: Boolean = true): TlvMap =
-    _readTLVMap(true, tagSize, suppressDuplication)
-
-@Suppress("DuplicatedCode", "FunctionName")
-public fun Input._readTLVMap(
-    expectingEOF: Boolean = true,
-    tagSize: Int,
-    suppressDuplication: Boolean = true
-): TlvMap {
-    val map = mutableMapOf<Int, ByteArray>()
-    var key = 0
-
-    while (kotlin.run {
-            try {
-                key = when (tagSize) {
-                    1 -> readUByte().toInt()
-                    2 -> readUShort().toInt()
-                    4 -> readUInt().toInt()
-                    else -> error("Unsupported tag size: $tagSize")
-                }
-            } catch (e: Exception) { // java.nio.BufferUnderflowException is not a EOFException...
-                if (expectingEOF) {
-                    return map
-                }
-                throw e
-            }
-            key
-        }.toUByte() != UByte.MAX_VALUE) {
-
-        if (map.containsKey(key)) {
-            @Suppress("ControlFlowWithEmptyBody")
-            if (!suppressDuplication) {
-                /*
-                @Suppress("DEPRECATION")
-                MiraiLogger.error(
-                    @Suppress("IMPLICIT_CAST_TO_ANY")
-                    """
-                Error readTLVMap:
-                duplicated key ${when (tagSize) {
-                        1 -> key.toByte()
-                        2 -> key.toShort()
-                        4 -> key
-                        else -> error("unreachable")
-                    }.contentToString()}
-                map=${map.contentToString()}
-                duplicating value=${this.readUShortLVByteArray().toUHexString()}
-                """.trimIndent()
-                )*/
-            } else {
-                this.discardExact(this.readShort().toInt() and 0xffff)
-            }
-        } else {
-            try {
-                map[key] = this.readBytes(readUShort().toInt())
-            } catch (e: Exception) { // BufferUnderflowException, java.io.EOFException
-                // if (expectingEOF) {
-                //     return map
-                // }
-                throw e
-            }
-        }
-    }
-    return map
-}
-
 public fun Input.readAllText(): String = Charsets.UTF_8.newDecoder().decode(this)
 
 public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String =

+ 91 - 0
mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmStatic
+
+/**
+ * 核心数据保护器
+ *
+ * ### Why
+ *
+ * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug.
+ * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码)
+ *
+ * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于
+ *
+ * - `sun.misc.Unsafe.allocate()`
+ * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`)
+ * - C/C++ (或其他语言) 的数据
+ *
+ * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`*
+ *
+ * ### How it works
+ *
+ * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来.
+ * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据
+ */
+@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax")
+//@MiraiExperimentalApi
+public object SecretsProtection {
+
+    @JvmInline
+    @Serializable(EscapedStringSerializer::class)
+    public value class EscapedString(
+        public val data: Any,
+    ) {
+        public val asString: String
+            get() = SecretsProtectionPlatform.impl_asString(data)
+
+        public constructor(data: ByteArray) : this(escape(data))
+        public constructor(data: String) : this(escape(data.encodeToByteArray()))
+    }
+
+    @JvmInline
+    @Serializable(EscapedByteBufferSerializer::class)
+    public value class EscapedByteBuffer(
+        public val data: Any,
+    ) {
+        public val size: Int get() = SecretsProtectionPlatform.impl_getSize(data)
+
+        public val asByteArray: ByteArray
+            get() = SecretsProtectionPlatform.impl_asByteArray(data)
+
+        public constructor(data: ByteArray) : this(escape(data))
+    }
+
+    @JvmStatic
+    public fun escape(data: ByteArray): Any {
+        return SecretsProtectionPlatform.escape(data)
+    }
+
+
+    public object EscapedStringSerializer :
+        KSerializer<EscapedString> by SecretsProtectionPlatform.EscapedStringSerializer
+
+    public object EscapedByteBufferSerializer :
+        KSerializer<EscapedByteBuffer> by SecretsProtectionPlatform.EscapedByteBufferSerializer
+}
+
+
+internal expect object SecretsProtectionPlatform {
+    fun impl_asString(data: Any): String
+    fun impl_asByteArray(data: Any): ByteArray
+    fun impl_getSize(data: Any): Int
+
+    fun escape(data: ByteArray): Any
+
+    object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString>
+
+    object EscapedByteBufferSerializer : KSerializer<SecretsProtection.EscapedByteBuffer>
+}

+ 67 - 0
mirai-core-utils/src/commonMain/kotlin/Services.kt

@@ -11,9 +11,76 @@
 
 package net.mamoe.mirai.utils
 
+import kotlinx.atomicfu.locks.reentrantLock
+import kotlinx.atomicfu.locks.withLock
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 
+public object Services {
+    private val lock = reentrantLock()
+    public fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>): String =
+        clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")
+
+    internal class Implementation(
+        val implementationClass: String,
+        val instance: Lazy<Any>
+    )
+
+    private val registered: MutableMap<String, MutableList<Implementation>> = mutableMapOf()
+    private val overrided: MutableMap<String, Implementation> = mutableMapOf()
+
+    @Suppress("UNCHECKED_CAST")
+    public fun <T : Any> getOverrideOrNull(clazz: KClass<out T>): T? {
+        lock.withLock {
+            return overrided[qualifiedNameOrFail(clazz)]?.instance?.value as T?
+        }
+    }
+
+    internal fun registerAsOverride(baseClass: String, implementationClass: String, implementation: () -> Any) {
+        lock.withLock {
+            overrided[baseClass] = Implementation(implementationClass, lazy(implementation))
+        }
+    }
+
+    public fun register(baseClass: String, implementationClass: String, implementation: () -> Any) {
+        lock.withLock {
+            registered.getOrPut(baseClass, ::mutableListOf)
+                .add(Implementation(implementationClass, lazy(implementation)))
+        }
+    }
+
+    public fun firstImplementationOrNull(baseClass: String): Any? {
+        lock.withLock {
+            overrided[baseClass]?.let { return it.instance.value }
+            return registered[baseClass]?.firstOrNull()?.instance?.value
+        }
+    }
+
+    public fun implementations(baseClass: String): Sequence<Lazy<Any>>? {
+        lock.withLock {
+            val implementations = registered[baseClass]
+            val forced = overrided[baseClass]
+            if (forced == null && implementations == null) return null
+
+            val implementationsSnapshot = implementations?.toList().orEmpty()
+
+            return sequence {
+                if (forced != null) yield(forced.instance)
+
+                implementationsSnapshot.forEach { yield(it.instance) }
+            }
+        }
+    }
+
+    internal fun implementationsDirectly(baseClass: String) = lock.withLock { registered[baseClass]?.toList().orEmpty() }
+
+    public fun print(): String {
+        lock.withLock {
+            return registered.entries.joinToString { "${it.key}:${it.value}" }
+        }
+    }
+}
+
 public expect fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String? = null): T?
 public expect fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String? = null): T
 public expect fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T>

+ 225 - 0
mirai-core-utils/src/commonMain/kotlin/TlvMap.kt

@@ -0,0 +1,225 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import io.ktor.utils.io.core.*
+import kotlin.jvm.JvmField
+
+public typealias TlvMap = MutableMap<Int, ByteArray>
+
+public fun TlvMap(): TlvMap = linkedMapOf()
+
+@Suppress("FunctionName")
+public fun Output._writeTlvMap(
+    tagSize: Int,
+    includeCount: Boolean = true,
+    map: TlvMap,
+) {
+    if (includeCount) {
+        when (tagSize) {
+            1 -> writeByte(map.size.toByte())
+            2 -> writeShort(map.size.toShort())
+            4 -> writeInt(map.size)
+            else -> error("Unsupported tag size: $tagSize")
+        }
+    }
+
+    map.forEach { (key, value) ->
+        when (tagSize) {
+            1 -> writeByte(key.toByte())
+            2 -> writeShort(key.toShort())
+            4 -> writeInt(key)
+            else -> error("Unsupported tag size: $tagSize")
+        }
+
+        writeShort(value.size.toShort())
+        writeFully(value)
+    }
+}
+
+@Suppress("MemberVisibilityCanBePrivate")
+public class TlvMapWriter
+internal constructor(
+    private val tagSize: Int,
+) {
+    @JvmField
+    internal val buffer = BytePacketBuilder()
+
+    @JvmField
+    internal var counter: Int = 0
+
+    @PublishedApi
+    @JvmField
+    internal var isWriting: Boolean = false
+
+    private fun writeKey(key: Int) {
+        when (tagSize) {
+            1 -> buffer.writeByte(key.toByte())
+            2 -> buffer.writeShort(key.toShort())
+            4 -> buffer.writeInt(key)
+            else -> error("Unsupported tag size: $tagSize")
+        }
+        counter++
+    }
+
+    @PublishedApi
+    internal fun ensureNotWriting() {
+        if (isWriting) error("Cannot write a new Tlv when writing Tlv")
+    }
+
+    public fun tlv(key: Int, data: ByteArray) {
+        ensureNotWriting()
+        tlv0(key, data)
+    }
+
+
+    private fun tlv0(key: Int, data: ByteArray) {
+        writeKey(key)
+        buffer.writeShort(data.size.toShort())
+        buffer.writeFully(data)
+//        println("Writing [${key.toUHexString()}](${data.size}) => " + data.toUHexString())
+    }
+
+    public fun tlv(key: Int, data: ByteReadPacket) {
+        ensureNotWriting()
+        tlv0(key, data)
+    }
+
+
+    @PublishedApi
+    internal fun tlv0(key: Int, data: ByteReadPacket) {
+        writeKey(key)
+        buffer.writeShort(data.remaining.toShort())
+
+//        println("Writing [${key.toUHexString()}](${data.remaining}) => " + data.copy()
+//            .use { d1 -> d1.readBytes().toUHexString() })
+
+        buffer.writePacket(data)
+    }
+
+
+    public inline fun tlv(
+        key: Int,
+        crossinline builder: BytePacketBuilder.() -> Unit,
+    ) {
+        ensureNotWriting()
+        try {
+            isWriting = true
+            buildPacket(builder).use { tlv0(key, it) }
+        } finally {
+            isWriting = false
+        }
+    }
+
+}
+
+public fun Output._writeTlvMap(
+    tagSize: Int = 2,
+    includeCount: Boolean = true,
+    block: TlvMapWriter.() -> Unit
+) {
+    val writer = TlvMapWriter(tagSize)
+    try {
+        block(writer)
+        if (includeCount) {
+            when (tagSize) {
+                1 -> writeByte(writer.counter.toByte())
+                2 -> writeShort(writer.counter.toShort())
+                4 -> writeInt(writer.counter)
+                else -> error("Unsupported tag size: $tagSize")
+            }
+        }
+        writer.buffer.build().use {
+//            println(it.copy().use { it.readBytes().toUHexString() })
+
+            writePacket(it)
+        }
+    } finally {
+        writer.buffer.release()
+    }
+}
+
+public fun TlvMap.getOrFail(tag: Int): ByteArray {
+    return this[tag] ?: error("cannot find tlv 0x${tag.toUHexString("")}($tag)")
+}
+
+public fun TlvMap.getOrFail(tag: Int, lazyMessage: (tag: Int) -> String): ByteArray {
+    return this[tag] ?: error(lazyMessage(tag))
+}
+
+@Suppress("FunctionName")
+public fun Input._readTLVMap(tagSize: Int = 2, suppressDuplication: Boolean = true): TlvMap =
+    _readTLVMap(true, tagSize, suppressDuplication)
+
+@Suppress("DuplicatedCode", "FunctionName")
+public fun Input._readTLVMap(
+    expectingEOF: Boolean = true,
+    tagSize: Int,
+    suppressDuplication: Boolean = true
+): TlvMap {
+    val map = linkedMapOf<Int, ByteArray>()
+    var key = 0
+
+    while (kotlin.run {
+            try {
+                key = when (tagSize) {
+                    1 -> readUByte().toInt()
+                    2 -> readUShort().toInt()
+                    4 -> readUInt().toInt()
+                    else -> error("Unsupported tag size: $tagSize")
+                }
+            } catch (e: Exception) { // java.nio.BufferUnderflowException is not a EOFException...
+                if (expectingEOF) {
+                    return map
+                }
+                throw e
+            }
+            key
+        }.toUByte() != UByte.MAX_VALUE) {
+
+        if (map.containsKey(key)) {
+//            println("reading ${key.toUHexString()}")
+
+            if (!suppressDuplication) {
+                /*
+                @Suppress("DEPRECATION")
+                MiraiLogger.error(
+                    @Suppress("IMPLICIT_CAST_TO_ANY")
+                    """
+                Error readTLVMap:
+                duplicated key ${when (tagSize) {
+                        1 -> key.toByte()
+                        2 -> key.toShort()
+                        4 -> key
+                        else -> error("unreachable")
+                    }.contentToString()}
+                map=${map.contentToString()}
+                duplicating value=${this.readUShortLVByteArray().toUHexString()}
+                """.trimIndent()
+                )*/
+            } else {
+                this.discardExact(this.readShort().toInt() and 0xffff)
+            }
+        } else {
+            try {
+                val len = readUShort().toInt()
+                val data = this.readBytes(len)
+//                println("Writing [${key.toUHexString()}]($len) => ${data.toUHexString()}")
+                map[key] = data
+            } catch (e: Exception) { // BufferUnderflowException, java.io.EOFException
+                // if (expectingEOF) {
+                //     return map
+                // }
+                throw e
+            }
+        }
+    }
+    return map
+}

+ 135 - 0
mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/TlvMapTest.kt

@@ -0,0 +1,135 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import io.ktor.utils.io.core.*
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+internal class TlvMapTest {
+    private fun dumpTlvMap(map: TlvMap) = buildString {
+        append("tlvMap {\n")
+        map.forEach { (k, v) ->
+            append("  ").append(k.toUHexString()).append(" = ").append(v.toUHexString()).append("\n")
+        }
+        append("}")
+    }
+
+    private fun assertTlvMapEquals(
+        expected: TlvMap, actual: TlvMap,
+    ) {
+        assertEquals(expected.size, actual.size, "map size not match")
+
+        expected.keys.forEach { key ->
+            assertTrue("Missing key[$key] in actual") { actual.containsKey(key) }
+        }
+        actual.keys.forEach { key ->
+            assertTrue("Missing key[$key] in expected") { expected.containsKey(key) }
+        }
+
+        expected.forEach { (key, value) ->
+            assertContentEquals(value, actual[key])
+        }
+    }
+
+    @Test
+    fun testTlvWriterNoLength() {
+        testTlvWriter(true)
+    }
+
+    @Test
+    fun testTlvWriterWithCount() {
+        testTlvWriter(false)
+    }
+
+    private fun testTlvWriter(withCount: Boolean) {
+        repeat(500) {
+            val tlvMap = TlvMap()
+            val rand = buildPacket {
+                _writeTlvMap(Short.SIZE_BYTES, includeCount = withCount) {
+
+                    repeat(Random.nextInt().and(0xFF).coerceAtLeast(20)) {
+                        val nextKey = Random.nextInt().and(0xFF0)
+                        if (!tlvMap.containsKey(nextKey)) {
+                            val randData = ByteArray(Random.nextInt().and(0xFFF))
+                            Random.nextBytes(randData)
+
+                            tlvMap[nextKey] = randData
+
+                            tlv(nextKey, randData)
+                        }
+                    }
+
+                }
+            }.also { pkg ->
+                if (withCount) pkg.discardExact(2)
+            }._readTLVMap()
+
+            try {
+                assertTlvMapEquals(tlvMap, rand)
+            } catch (e: Throwable) {
+
+                println("gen:  " + dumpTlvMap(tlvMap))
+                println("read: " + dumpTlvMap(rand))
+
+                throw e
+            }
+
+        }
+    }
+
+    @Test
+    fun testTlvWriterWithCounter() {
+        val expected = buildPacket {
+            writeShort(4) // count of TLVs
+
+            writeShort(0x01)
+            writeHexWithLength("66ccff")
+
+            writeShort(0x04)
+            writeHexWithLength("114514")
+
+            writeShort(0x19)
+            writeHexWithLength("198100")
+
+            writeShort(0x233)
+            writeHexWithLength("666666")
+        }.readBytes()
+
+        val actual = buildPacket {
+            _writeTlvMap {
+                tlv(0x001) { writeHex("66ccff") }
+                tlv(0x004) { writeHex("114514") }
+                tlv(0x019) { writeHex("198100") }
+                tlv(0x233) { writeHex("666666") }
+
+                println("counter = $counter")
+            }
+        }.readBytes()
+
+        println(expected.toUHexString())
+        println(actual.toUHexString())
+
+        assertContentEquals(expected, actual)
+    }
+
+    private fun Output.writeHex(data: String) {
+        writeFully(data.hexToBytes())
+    }
+
+    private fun Output.writeHexWithLength(data: String) {
+        val hxd = data.hexToBytes()
+        writeShort(hxd.size.toShort())
+        writeFully(hxd)
+    }
+}

+ 14 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/Collections.kt

@@ -54,4 +54,18 @@ public actual fun <K : Enum<K>, V> EnumMap(clazz: KClass<K>): MutableMap<K, V> {
 @Suppress("FunctionName")
 public actual fun <E> ConcurrentSet(): MutableSet<E> {
     return CopyOnWriteArraySet()
+}
+
+/**
+ * Same as [MutableCollection.addAll].
+ *
+ * Adds all the elements of the specified enumeration to this collection.
+ * @return true if any of the specified elements was added to the collection, false if the collection was not modified.
+ */
+public fun <T> MutableCollection<T>.addAll(enumeration: Enumeration<T>): Boolean {
+    var addResult = false
+    while (enumeration.hasMoreElements()) {
+        addResult = this.add(enumeration.nextElement())
+    }
+    return addResult
 }

+ 26 - 49
mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt

@@ -10,39 +10,15 @@
 package net.mamoe.mirai.utils
 
 import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.ByteArraySerializer
 import kotlinx.serialization.builtins.serializer
 import java.nio.ByteBuffer
-import java.util.concurrent.ConcurrentLinkedDeque
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater
 import java.util.concurrent.locks.Lock
 import java.util.concurrent.locks.ReentrantLock
 
-/**
- * 核心数据保护器
- *
- * ### Why
- *
- * 有时候可能会发生 `OutOfMemoryError`, 如果存在 `-XX:+HeapDumpOnOutOfMemoryError`, 则 JVM 会生成一份系统内存打印以供 debug.
- * 该报告包含全部内存信息, 包括各种数据, 核心数据以及, 机密数据 (如密码)
- *
- * 该内存报告唯一没有包含的数据就是 Native层数据, 包括且不限于
- *
- * - `sun.misc.Unsafe.allocate()`
- * - `java.nio.ByteBuffer.allocateDirect()` (Named `DirectByteBuffer`)
- * - C/C++ (或其他语言) 的数据
- *
- * *试验数据来源 `openjdk version "17" 2021-09-14, 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)`*
- *
- * ### How it works
- *
- * 因为 Heap Dump 不存在 `DirectByteBuffer` 的实际数据, 所以可以通过该类隐藏关键数据. 等需要的时候在读取出来.
- * 因为数据并没有直接存在于某个类字段中, 缺少数据关联, 也很难分析相关数据是什么数据
- */
-@Suppress("NOTHING_TO_INLINE", "UsePropertyAccessSyntax")
-//@MiraiExperimentalApi
-public object SecretsProtection {
+internal actual object SecretsProtectionPlatform {
+
     private class NativeBufferWithLock(
         @JvmField val buffer: ByteBuffer,
         val lock: Lock = ReentrantLock(),
@@ -106,7 +82,7 @@ public object SecretsProtection {
 
      */
     @JvmStatic
-    public fun allocate(size: Int): ByteBuffer {
+    fun allocate(size: Int): ByteBuffer {
         if (size >= bufferSize) {
             return ByteBuffer.allocateDirect(size)
         }
@@ -171,39 +147,40 @@ public object SecretsProtection {
     }
 
     @JvmStatic
-    public fun escape(data: ByteArray): ByteBuffer {
+    actual fun escape(data: ByteArray): Any {
         return allocate(data.size).also {
             it.put(data)
             it.pos = 0
         }
     }
 
-    @JvmInline
-    @Serializable(EscapedStringSerializer::class)
-    public value class EscapedString(
-        public val data: ByteBuffer,
-    ) {
-        public val asString: String
-            get() = data.duplicate().readString()
+    actual fun impl_asString(data: Any): String {
+        data as ByteBuffer
+
+        return data.duplicate().readString()
     }
 
-    @JvmInline
-    @Serializable(EscapedByteBufferSerializer::class)
-    public value class EscapedByteBuffer(
-        public val data: ByteBuffer,
-    )
+    actual fun impl_asByteArray(data: Any): ByteArray {
+        data as ByteBuffer
+        return data.duplicate().readBytes()
+    }
+
+    actual fun impl_getSize(data: Any): Int {
+        return (data as ByteBuffer).remaining
+    }
 
-    public object EscapedStringSerializer : KSerializer<EscapedString> by String.serializer().map(
+    actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> by String.serializer().map(
         String.serializer().descriptor.copy("EscapedString"),
-        deserialize = { EscapedString(escape(it.toByteArray())) },
-        serialize = { it.data.duplicate().readString() }
+        deserialize = { SecretsProtection.EscapedString(escape(it.toByteArray())) },
+        serialize = { it.data.cast<ByteBuffer>().duplicate().readString() }
     )
 
-    public object EscapedByteBufferSerializer : KSerializer<EscapedByteBuffer> by ByteArraySerializer().map(
-        ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
-        deserialize = { EscapedByteBuffer(escape(it)) },
-        serialize = { it.data.duplicate().readBytes() }
-    )
+    actual object EscapedByteBufferSerializer :
+        KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
+            ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
+            deserialize = { SecretsProtection.EscapedByteBuffer(escape(it)) },
+            serialize = { it.data.cast<ByteBuffer>().duplicate().readBytes() }
+        )
 
 
-}
+}

+ 77 - 12
mirai-core-utils/src/jvmBaseMain/kotlin/Services.kt

@@ -13,15 +13,51 @@ import java.util.*
 import kotlin.reflect.KClass
 import kotlin.reflect.full.createInstance
 
+private enum class LoaderType {
+    JDK,
+    BOTH,
+    FALLBACK,
+}
+
+private val loaderType = when (systemProp("mirai.service.loader", "both")) {
+    "jdk" -> LoaderType.JDK
+    "both" -> LoaderType.BOTH
+    "fallback" -> LoaderType.FALLBACK
+    else -> throw IllegalStateException("cannot find a service loader, mirai.service.loader must be both, jdk or fallback (default by both)")
+}
+
+@Suppress("UNCHECKED_CAST")
 public actual fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String?): T {
+    val fallbackService by lazy {
+        Services.firstImplementationOrNull(Services.qualifiedNameOrFail(clazz)) as T?
+    }
+
+    val jdkService by lazy {
+        ServiceLoader.load(clazz.java).firstOrNull()?.let { return@lazy it }
+
+        ServiceLoader.load(clazz.java, clazz.java.classLoader).firstOrNull()
+    }
+
     var suppressed: Throwable? = null
-    return ServiceLoader.load(clazz.java).firstOrNull()
-        ?: ServiceLoader.load(clazz.java, clazz.java.classLoader).firstOrNull()
-        ?: (if (fallbackImplementation == null) null
-        else runCatching { findCreateInstance<T>(fallbackImplementation) }.onFailure { suppressed = it }.getOrNull())
-        ?: throw NoSuchElementException("Could not find an implementation for service class ${clazz.qualifiedName}").apply {
-            if (suppressed != null) addSuppressed(suppressed)
-        }
+
+    val services by lazy {
+        when (loaderType) {
+            LoaderType.JDK -> jdkService
+            LoaderType.BOTH -> jdkService ?: fallbackService
+            LoaderType.FALLBACK -> fallbackService
+        }?.let { return@lazy it }
+
+        if (fallbackImplementation != null) {
+            runCatching {
+                findCreateInstance<T>(fallbackImplementation)
+            }.onFailure { suppressed = it }.getOrNull()
+        } else null
+    }
+
+    return Services.getOverrideOrNull(clazz) ?: services
+    ?: throw NoSuchElementException("Could not find an implementation for service class ${clazz.qualifiedName}").apply {
+        if (suppressed != null) addSuppressed(suppressed)
+    }
 }
 
 private fun <T : Any> findCreateInstance(fallbackImplementation: String): T {
@@ -29,14 +65,17 @@ private fun <T : Any> findCreateInstance(fallbackImplementation: String): T {
 }
 
 public actual fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String?): T? {
-    return ServiceLoader.load(clazz.java).firstOrNull()
-        ?: ServiceLoader.load(clazz.java, clazz.java.classLoader).firstOrNull()
-        ?: if (fallbackImplementation == null) return null
-        else runCatching { findCreateInstance<T>(fallbackImplementation) }.getOrNull()
+    return runCatching { loadService(clazz, fallbackImplementation) }.getOrNull()
 }
 
+@Suppress("UNCHECKED_CAST")
 public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> {
-    return sequence {
+    fun fallBackServicesSeq(): Sequence<T> {
+        return Services.implementations(Services.qualifiedNameOrFail(clazz)).orEmpty()
+            .map { it.value as T }
+    }
+
+    fun jdkServices(): Sequence<T> = sequence {
         val current = ServiceLoader.load(clazz.java).iterator()
         if (current.hasNext()) {
             yieldAll(current)
@@ -44,4 +83,30 @@ public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> {
             yieldAll(ServiceLoader.load(clazz.java, clazz.java.classLoader))
         }
     }
+
+    fun bothServices(): Sequence<T> = sequence {
+        Services.getOverrideOrNull(clazz)?.let { yield(it) }
+
+        var jdkServices = ServiceLoader.load(clazz.java).toList()
+        if (jdkServices.isEmpty()) {
+            jdkServices = ServiceLoader.load(clazz.java, clazz.java.classLoader).toList()
+        }
+        yieldAll(jdkServices)
+
+        Services.implementationsDirectly(Services.qualifiedNameOrFail(clazz)).asSequence()
+            .filter { impl ->
+                // Drop duplicated
+                jdkServices.none { it.javaClass.name == impl.implementationClass }
+            }
+            .forEach { yield(it.instance.value as T) }
+    }
+
+
+
+
+    return when (loaderType) {
+        LoaderType.JDK -> jdkServices()
+        LoaderType.BOTH -> bothServices()
+        LoaderType.FALLBACK -> fallBackServicesSeq()
+    }
 }

+ 3 - 3
mirai-core-utils/src/jvmMain/kotlin/IO.jvm.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -39,10 +39,10 @@ public fun Path.mkParentDirs() {
     current.mkdir()
 }
 
-public fun Path.deleteRecursively(): Boolean {
+public fun Path.deleteRecursivelyMirai(): Boolean { // Kotlin added `Path.deleteRecursively()` in 1.8.0 but was experimental
     if (isFile) return deleteIfExists()
     if (isDirectory()) {
-        listDirectoryEntries().forEach { it.deleteRecursively() }
+        listDirectoryEntries().forEach { it.deleteRecursivelyMirai() }
         return deleteIfExists()
     }
     return false

+ 2 - 2
mirai-core-utils/src/jvmTest/kotlin/SecretsProtectionTest.kt

@@ -12,9 +12,9 @@ package net.mamoe.mirai.utils
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import java.nio.ByteBuffer
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
-import kotlin.test.assertTrue
 
 internal class SecretsProtectionTest {
     @Test
@@ -22,7 +22,7 @@ internal class SecretsProtectionTest {
         repeat(500) {
             launch {
                 val data = ByteArray((1..255).random()) { (0..255).random().toByte() }
-                val buffer = SecretsProtection.escape(data)
+                val buffer = SecretsProtection.escape(data) as ByteBuffer
                 assertContentEquals(
                     data, buffer.duplicate().readBytes()
                 )

+ 47 - 0
mirai-core-utils/src/nativeMain/kotlin/SecretsProtection.kt

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019-2023 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/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.ByteArraySerializer
+import kotlinx.serialization.builtins.serializer
+
+internal actual object SecretsProtectionPlatform {
+    actual fun impl_asString(data: Any): String {
+        return (data as ByteArray).decodeToString()
+    }
+
+    actual fun impl_asByteArray(data: Any): ByteArray {
+        return data as ByteArray
+    }
+
+    actual fun impl_getSize(data: Any): Int {
+        return data.cast<ByteArray>().size
+    }
+
+    actual fun escape(data: ByteArray): Any {
+        return data
+    }
+
+    actual object EscapedStringSerializer : KSerializer<SecretsProtection.EscapedString> by String.serializer().map(
+        String.serializer().descriptor.copy("EscapedString"),
+        deserialize = { SecretsProtection.EscapedString(it.encodeToByteArray()) },
+        serialize = { it.data.cast<ByteArray>().decodeToString() }
+    )
+
+    actual object EscapedByteBufferSerializer :
+        KSerializer<SecretsProtection.EscapedByteBuffer> by ByteArraySerializer().map(
+            ByteArraySerializer().descriptor.copy("EscapedByteBuffer"),
+            deserialize = { SecretsProtection.EscapedByteBuffer(it) },
+            serialize = { it.data.cast() }
+        )
+
+
+}

+ 2 - 43
mirai-core-utils/src/nativeMain/kotlin/Service.kt

@@ -11,47 +11,9 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.atomicfu.locks.reentrantLock
-import kotlinx.atomicfu.locks.withLock
+import net.mamoe.mirai.utils.Services.qualifiedNameOrFail
 import kotlin.reflect.KClass
 
-public object Services {
-    private val lock = reentrantLock()
-
-    private class Implementation(
-        val implementationClass: String,
-        val instance: Lazy<Any>
-    )
-
-    private val registered: MutableMap<String, MutableList<Implementation>> = mutableMapOf()
-
-    public fun register(baseClass: String, implementationClass: String, implementation: () -> Any) {
-        lock.withLock {
-            registered.getOrPut(baseClass, ::mutableListOf)
-                .add(Implementation(implementationClass, lazy(implementation)))
-        }
-    }
-
-    public fun firstImplementationOrNull(baseClass: String): Any? {
-        lock.withLock {
-            return registered[baseClass]?.firstOrNull()?.instance?.value
-        }
-    }
-
-    public fun implementations(baseClass: String): List<Lazy<Any>>? {
-        lock.withLock {
-            return registered[baseClass]?.map { it.instance }
-        }
-
-    }
-
-    public fun print(): String {
-        lock.withLock {
-            return registered.entries.joinToString { "${it.key}:${it.value}" }
-        }
-    }
-}
-
 @Suppress("UNCHECKED_CAST")
 public actual fun <T : Any> loadServiceOrNull(
     clazz: KClass<out T>,
@@ -66,7 +28,4 @@ public actual fun <T : Any> loadService(
     ?: error("Could not load service '${clazz.qualifiedName ?: clazz}'. Current services: ${Services.print()}")
 
 public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> =
-    Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp()
-
-private fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>) =
-    clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")
+    Services.implementations(qualifiedNameOrFail(clazz)).orEmpty().map { it.value }.castUp()

+ 19 - 9
mirai-core/src/commonMain/kotlin/BotAccount.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
+ * Copyright 2019-2023 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.
@@ -10,16 +10,26 @@
 
 package net.mamoe.mirai.internal
 
+import net.mamoe.mirai.auth.BotAuthorization
+import net.mamoe.mirai.utils.SecretsProtection
+import net.mamoe.mirai.utils.TestOnly
 
-internal expect class BotAccount {
-    internal val id: Long
-    val phoneNumber: String
 
-    constructor(id: Long, passwordMd5: ByteArray, phoneNumber: String = "")
-    constructor(id: Long, passwordPlainText: String, phoneNumber: String = "")
+internal class BotAccount(
+    internal val id: Long,
+    val authorization: BotAuthorization,
+) {
+    @TestOnly // to be compatible with your local tests :)
+    constructor(
+        id: Long, pwd: String
+    ) : this(id, BotAuthorization.byPassword(pwd))
 
-    val passwordMd5: ByteArray
+    var accountSecretsKeyBuffer: SecretsProtection.EscapedByteBuffer? = null
+
+    val accountSecretsKey: ByteArray
+        get() {
+            accountSecretsKeyBuffer?.let { return it.asByteArray }
+            error("accountSecretsKey not yet available")
+        }
 
-    override fun equals(other: Any?): Boolean
-    override fun hashCode(): Int
 }

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