소스 검색

Commonize projects: mirai-core series, and mirai-console-compiler-annotations

Him188 3 년 전
부모
커밋
0571be1a55
100개의 변경된 파일5932개의 추가작업 그리고 1545개의 파일을 삭제
  1. 2 2
      buildSrc/src/main/kotlin/Versions.kt
  2. 2 2
      mirai-console/tools/compiler-annotations/src/commonMain/kotlin/CheckerConstants.kt
  3. 10 0
      mirai-core-api/src/androidMain/kotlin/package.kt
  4. 5 6
      mirai-core-api/src/androidMain/kotlin/utils/LoginSolver.android.kt
  5. 5 3
      mirai-core-api/src/commonMain/kotlin/Bot.kt
  6. 3 16
      mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt
  7. 2 3
      mirai-core-api/src/commonMain/kotlin/message/data/CombinedMessage.kt
  8. 7 3
      mirai-core-api/src/commonMain/kotlin/message/data/Image.kt
  9. 0 1
      mirai-core-api/src/commonMain/kotlin/message/data/impl.kt
  10. 5 4
      mirai-core-api/src/commonMain/kotlin/network/ForceOfflineException.kt
  11. 7 5
      mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt
  12. 9 17
      mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt
  13. 41 202
      mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt
  14. 132 132
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt
  15. 18 593
      mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt
  16. 6 5
      mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt
  17. 1 9
      mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt
  18. 26 68
      mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt
  19. 4 1
      mirai-core-api/src/commonMain/kotlin/utils/OverFileSizeMaxException.kt
  20. 2 1
      mirai-core-api/src/commonMain/kotlin/utils/ProgressionCallback.kt
  21. 69 148
      mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt
  22. 1 1
      mirai-core-api/src/commonTest/kotlin/message.data/ConstrainSingleHelperTest.kt
  23. 1 1
      mirai-core-api/src/commonTest/kotlin/message.data/LinearMessageChainImplTest.kt
  24. 1 1
      mirai-core-api/src/commonTest/kotlin/message.data/MessageChainImplTest.kt
  25. 1 1
      mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt
  26. 111 0
      mirai-core-api/src/jvmBaseMain/kotlin/contact/announcement/Announcements.kt
  27. 11 0
      mirai-core-api/src/jvmBaseMain/kotlin/contact/package.kt
  28. 8 0
      mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceImpls.kt
  29. 10 0
      mirai-core-api/src/jvmBaseMain/kotlin/package.kt
  30. 39 0
      mirai-core-api/src/jvmBaseMain/kotlin/spi/SPIServiceLoader.kt
  31. 257 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/AbstractExternalResource.kt
  32. 656 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt
  33. 142 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt
  34. 623 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/ExternalResource.kt
  35. 4 4
      mirai-core-api/src/jvmBaseMain/kotlin/utils/FileLogger.kt
  36. 0 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/LoggerAdapters.kt
  37. 302 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt
  38. 667 0
      mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt
  39. 4 4
      mirai-core-api/src/jvmBaseMain/kotlin/utils/SingleFileLogger.kt
  40. 4 4
      mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt
  41. 10 0
      mirai-core-api/src/jvmMain/kotlin/package.kt
  42. 95 0
      mirai-core-api/src/nativeMain/kotlin/contact/announcement/Announcements.kt
  43. 10 0
      mirai-core-api/src/nativeMain/kotlin/package.kt
  44. 35 0
      mirai-core-api/src/nativeMain/kotlin/spi/SPIServiceLoader.kt
  45. 554 0
      mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt
  46. 121 0
      mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt
  47. 273 0
      mirai-core-api/src/nativeMain/kotlin/utils/ExternalResource.kt
  48. 85 0
      mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt
  49. 254 0
      mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt
  50. 60 0
      mirai-core-api/src/nativeMain/kotlin/utils/PlatformLogger.kt
  51. 575 0
      mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt
  52. 3 0
      mirai-core-utils/src/commonMain/kotlin/Arrays.kt
  53. 1 4
      mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt
  54. 4 0
      mirai-core-utils/src/commonMain/kotlin/Bytes.kt
  55. 2 1
      mirai-core-utils/src/commonMain/kotlin/CheckableResult.kt
  56. 17 0
      mirai-core-utils/src/commonMain/kotlin/Closeable.kt
  57. 13 0
      mirai-core-utils/src/commonMain/kotlin/Collections.kt
  58. 8 5
      mirai-core-utils/src/commonMain/kotlin/ComputeOnNullMutableProperty.kt
  59. 3 0
      mirai-core-utils/src/commonMain/kotlin/Conversions.kt
  60. 7 17
      mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt
  61. 4 0
      mirai-core-utils/src/commonMain/kotlin/Either.kt
  62. 7 12
      mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt
  63. 7 4
      mirai-core-utils/src/commonMain/kotlin/Files.kt
  64. 8 24
      mirai-core-utils/src/commonMain/kotlin/IO.kt
  65. 8 6
      mirai-core-utils/src/commonMain/kotlin/LateinitMutableProperty.kt
  66. 18 110
      mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt
  67. 3 0
      mirai-core-utils/src/commonMain/kotlin/Numbers.kt
  68. 6 4
      mirai-core-utils/src/commonMain/kotlin/RandomUtils.kt
  69. 11 17
      mirai-core-utils/src/commonMain/kotlin/Resources.kt
  70. 6 4
      mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt
  71. 0 29
      mirai-core-utils/src/commonMain/kotlin/Serialization.kt
  72. 4 3
      mirai-core-utils/src/commonMain/kotlin/SizedCache.kt
  73. 22 33
      mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt
  74. 11 0
      mirai-core-utils/src/commonMain/kotlin/Strings.kt
  75. 6 4
      mirai-core-utils/src/commonMain/kotlin/Symbol.kt
  76. 8 10
      mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt
  77. 1 1
      mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt
  78. 4 2
      mirai-core-utils/src/commonMain/kotlin/UnsafeMutableNonNullProperty.kt
  79. 10 8
      mirai-core-utils/src/commonMain/kotlin/systemProp.kt
  80. 12 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Closeable.kt
  81. 46 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Collections.kt
  82. 21 0
      mirai-core-utils/src/jvmBaseMain/kotlin/CoroutineUtils.kt
  83. 120 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Crypto.kt
  84. 20 0
      mirai-core-utils/src/jvmBaseMain/kotlin/ExceptionCollector.kt
  85. 34 0
      mirai-core-utils/src/jvmBaseMain/kotlin/File.kt
  86. 1 1
      mirai-core-utils/src/jvmBaseMain/kotlin/JvmNioBuffer.kt
  87. 1 1
      mirai-core-utils/src/jvmBaseMain/kotlin/Reflections.kt
  88. 16 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Resources.kt
  89. 1 2
      mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt
  90. 37 0
      mirai-core-utils/src/jvmBaseMain/kotlin/Serialization.kt
  91. 1 1
      mirai-core-utils/src/jvmBaseMain/kotlin/Streams.kt
  92. 1 1
      mirai-core-utils/src/jvmBaseMain/kotlin/ThreadLocal.kt
  93. 12 0
      mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt
  94. 4 4
      mirai-core-utils/src/jvmBaseMain/kotlin/WeakRef.kt
  95. 9 0
      mirai-core-utils/src/jvmBaseMain/kotlin/package.kt
  96. 12 0
      mirai-core-utils/src/jvmBaseMain/kotlin/systemProp.kt
  97. 10 0
      mirai-core-utils/src/jvmBaseTest/kotlin/package.kt
  98. 17 0
      mirai-core-utils/src/nativeMain/kotlin/Closeable.kt
  99. 15 0
      mirai-core-utils/src/nativeMain/kotlin/ConcurrentHashMap.kt
  100. 70 0
      mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt

+ 2 - 2
buildSrc/src/main/kotlin/Versions.kt

@@ -38,7 +38,7 @@ object Versions {
     const val io = "0.1.16"
     const val coroutinesIo = "0.1.16"
 
-    const val blockingBridge = "2.0.0-162.1"
+    const val blockingBridge = "2.1.0-162.1"
     const val dynamicDelegation = "0.3.0-162.4"
 
     const val androidGradlePlugin = "4.1.1"
@@ -58,7 +58,7 @@ object Versions {
 
     const val junit = "5.7.2"
 
-    const val yamlkt = "0.11.0"
+    const val yamlkt = "0.12.0"
     const val intellijGradlePlugin = "1.5.3"
 
     //    const val kotlinIntellijPlugin = "211-1.5.20-release-284-IJ7442.40" // keep to newest as kotlinCompiler

+ 2 - 2
mirai-console/tools/compiler-annotations/src/commonMain/kotlin/CheckerConstants.kt

@@ -9,13 +9,13 @@
 
 package net.mamoe.mirai.console.compiler.common
 
-import org.intellij.lang.annotations.Language
+import kotlin.jvm.JvmField
 
 /**
  * @suppress 这是内部 API. 可能在任意时刻变动
  */
 public object CheckerConstants {
-    @Language("RegExp")
+    //    @Language("RegExp")
     public const val PLUGIN_ID_PATTERN: String = """([a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)\.([a-zA-Z]\w*(?:-\w+)*)"""
 
     @JvmField

+ 10 - 0
mirai-core-api/src/androidMain/kotlin/package.kt

@@ -0,0 +1,10 @@
+/*
+ * 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

+ 5 - 6
mirai-core-api/src/androidMain/kotlin/utils/LoginSolver.android.kt

@@ -1,17 +1,16 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 
 package net.mamoe.mirai.utils
 
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
 import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.utils.LoginSolver.Companion.Default
 
@@ -38,7 +37,7 @@ public actual abstract class LoginSolver public actual constructor() {
      * 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
      */
     public actual open val isSliderCaptchaSupported: Boolean
-        get() = isSliderCaptchaSupportKind ?: true
+        get() = System.getProperty("mirai.slider.captcha.supported") != null
 
     /**
      * 处理滑动验证码.

+ 5 - 3
mirai-core-api/src/commonMain/kotlin/Bot.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -21,9 +21,11 @@ import net.mamoe.mirai.message.action.BotNudge
 import net.mamoe.mirai.message.action.MemberNudge
 import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.utils.BotConfiguration
+import net.mamoe.mirai.utils.ConcurrentHashMap
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.NotStableForInheritance
-import java.util.concurrent.ConcurrentHashMap
+import kotlin.jvm.JvmStatic
+import kotlin.jvm.JvmSynthetic
 
 /**
  * 登录, 返回 [this]
@@ -180,7 +182,7 @@ public interface Bot : CoroutineScope, ContactOrBot, UserOrBot {
 
     public companion object {
         @Suppress("ObjectPropertyName")
-        internal val _instances: ConcurrentHashMap<Long, Bot> = ConcurrentHashMap()
+        internal val _instances: MutableMap<Long, Bot> = ConcurrentHashMap()
 
         /**
          * 复制一份此时的 [Bot] 实例列表.

+ 3 - 16
mirai-core-api/src/commonMain/kotlin/contact/announcement/Announcements.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -7,19 +7,15 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:JvmBlockingBridge
 @file:Suppress("INAPPLICABLE_JVM_NAME")
 
 package net.mamoe.mirai.contact.announcement
 
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.toList
-import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.PermissionDeniedException
 import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.NotStableForInheritance
-import java.util.stream.Stream
 
 
 /**
@@ -44,7 +40,7 @@ import java.util.stream.Stream
  * @since 2.7
  */
 @NotStableForInheritance
-public interface Announcements {
+public expect interface Announcements {
     /**
      * 创建一个能获取该群内所有群公告列表的 [Flow]. 在 [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
      *
@@ -52,15 +48,6 @@ public interface Announcements {
      */
     public suspend fun asFlow(): Flow<OnlineAnnouncement>
 
-    /**
-     * 创建一个能获取该群内所有群公告列表的 [Stream]. 在 [Stream] 被使用时才会分页下载 [OnlineAnnouncement].
-     *
-     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] 和 [Stream] 的[收集][Stream.collect].
-     *
-     * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [asFlow].
-     */
-    public fun asStream(): Stream<OnlineAnnouncement>
-
     /**
      * 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
      *
@@ -68,7 +55,7 @@ public interface Announcements {
      *
      * @return 此时刻的群公告只读列表.
      */
-    public suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList()
+    public open suspend fun toList(): List<OnlineAnnouncement>
 
 
     /**

+ 2 - 3
mirai-core-api/src/commonMain/kotlin/message/data/CombinedMessage.kt

@@ -13,6 +13,7 @@ import net.mamoe.mirai.message.data.visitor.MessageVisitor
 import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor
 import net.mamoe.mirai.message.data.visitor.accept
 import net.mamoe.mirai.utils.MiraiInternalApi
+import net.mamoe.mirai.utils.isSameType
 
 /**
  * One after one, hierarchically.
@@ -166,9 +167,7 @@ public class CombinedMessage @MessageChainConstructor constructor(
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as CombinedMessage
+        if (!isSameType(this, other)) return false
 
         if (element != other.element) return false
         if (tail != other.tail) return false

+ 7 - 3
mirai-core-api/src/commonMain/kotlin/message/data/Image.kt

@@ -41,6 +41,10 @@ import net.mamoe.mirai.message.data.Image.Key.queryUrl
 import net.mamoe.mirai.message.data.visitor.MessageVisitor
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmStatic
+import kotlin.jvm.JvmSynthetic
 
 /**
  * 自定义表情 (收藏的表情) 和普通图片.
@@ -396,7 +400,7 @@ public interface Image : Message, MessageContent, CodableMessage {
  * @see IMirai.createImage
  */
 @JvmSynthetic
-public inline fun Image(imageId: String): Image = Image.Builder.newBuilder(imageId).build()
+public inline fun Image(imageId: String): Image = Builder.newBuilder(imageId).build()
 
 /**
  * 使用 [Image.Builder] 构建一个 [Image].
@@ -405,8 +409,8 @@ public inline fun Image(imageId: String): Image = Image.Builder.newBuilder(image
  * @since 2.9.0
  */
 @JvmSynthetic
-public inline fun Image(imageId: String, builderAction: Image.Builder.() -> Unit = {}): Image =
-    Image.Builder.newBuilder(imageId).apply(builderAction).build()
+public inline fun Image(imageId: String, builderAction: Builder.() -> Unit = {}): Image =
+    Builder.newBuilder(imageId).apply(builderAction).build()
 
 public enum class ImageType(
     /**

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

@@ -19,7 +19,6 @@ import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
 import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
 import net.mamoe.mirai.message.data.visitor.MessageVisitor
 import net.mamoe.mirai.utils.MiraiInternalApi
-import net.mamoe.mirai.utils.asImmutable
 import net.mamoe.mirai.utils.castOrNull
 import net.mamoe.mirai.utils.replaceAllKotlin
 

+ 5 - 4
mirai-core-api/src/commonMain/kotlin/network/ForceOfflineException.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.network
@@ -13,6 +13,7 @@ import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Job
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.utils.DeprecatedSinceMirai
+import kotlin.jvm.JvmOverloads
 
 /**
  * 当 [Bot] 被迫下线时抛出, 作为 [Job.cancel] 的 `cause`

+ 7 - 5
mirai-core-api/src/commonMain/kotlin/spi/AudioToSilkService.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -9,8 +9,10 @@
 
 package net.mamoe.mirai.spi
 
+import kotlinx.io.errors.IOException
 import net.mamoe.mirai.utils.*
-import java.io.IOException
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.jvm.JvmStatic
 
 /**
  * 将源音频文件转换为 silk v3 with tencent 格式
@@ -33,17 +35,17 @@ public interface AudioToSilkService : BaseService {
      * @see [runAutoClose]
      * @see [useAutoClose]
      */
-    @Throws(IOException::class)
+    @Throws(IOException::class, CancellationException::class)
     public suspend fun convert(source: ExternalResource): ExternalResource
 
     @MiraiExperimentalApi
     public companion object : AudioToSilkService {
         private val loader = SPIServiceLoader(object : AudioToSilkService {
             override suspend fun convert(source: ExternalResource): ExternalResource = source
-        }, AudioToSilkService::class.java)
+        }, AudioToSilkService::class)
 
         @Suppress("BlockingMethodInNonBlockingContext")
-        @Throws(IOException::class)
+        @Throws(IOException::class, CancellationException::class)
         override suspend fun convert(source: ExternalResource): ExternalResource {
             return loader.service.convert(source)
         }

+ 9 - 17
mirai-core-api/src/commonMain/kotlin/spi/SPIServiceLoader.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -11,7 +11,8 @@ package net.mamoe.mirai.spi
 
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.MiraiLogger
-import java.util.*
+import kotlin.jvm.JvmField
+import kotlin.reflect.KClass
 
 /**
  * 基本 SPI 接口
@@ -23,25 +24,16 @@ public interface BaseService {
     public val priority: Int get() = 5
 }
 
-internal class SPIServiceLoader<T : BaseService>(
-    @JvmField val defaultService: T,
-    @JvmField val serviceType: Class<T>,
+internal expect class SPIServiceLoader<T : BaseService>(
+    defaultService: T,
+    serviceType: KClass<T>,
 ) {
     @JvmField
-    var service: T = defaultService
+    var service: T
 
-    fun reload() {
-        val loader = ServiceLoader.load(serviceType)
-        service = loader.minByOrNull { it.priority } ?: defaultService
-    }
-
-    init {
-        reload()
-    }
+    fun reload()
 
     companion object {
-        val SPI_SERVICE_LOADER_LOGGER by lazy {
-            MiraiLogger.Factory.create(SPIServiceLoader::class.java, "spi-service-loader")
-        }
+        val SPI_SERVICE_LOADER_LOGGER: MiraiLogger
     }
 }

+ 41 - 202
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -17,17 +17,13 @@ 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 java.io.File
-import java.io.InputStream
 import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.EmptyCoroutineContext
 import kotlin.coroutines.coroutineContext
+import kotlin.jvm.*
 import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
 
 /**
  * [Bot] 配置. 用于 [BotFactory.newBot]
@@ -53,18 +49,13 @@ import kotlin.time.Duration.Companion.milliseconds
  * ```
  */
 @Suppress("PropertyName")
-public open class BotConfiguration { // open for Java
-    /**
-     * 工作目录. 默认为 "."
-     */
-    public var workingDir: File = File(".")
-
+public expect open class BotConfiguration() { // open for Java
     ///////////////////////////////////////////////////////////////////////////
     // Coroutines
     ///////////////////////////////////////////////////////////////////////////
 
     /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
-    public var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
+    public var parentCoroutineContext: CoroutineContext
 
     /**
      * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
@@ -120,9 +111,7 @@ public open class BotConfiguration { // open for Java
      */
     @JvmSynthetic
     @ConfigurationDsl
-    public suspend inline fun inheritCoroutineContext() {
-        parentCoroutineContext = coroutineContext
-    }
+    public suspend inline fun inheritCoroutineContext()
 
 
     ///////////////////////////////////////////////////////////////////////////
@@ -130,7 +119,7 @@ public open class BotConfiguration { // open for Java
     ///////////////////////////////////////////////////////////////////////////
 
     /** 连接心跳包周期. 过长会导致被服务器断开连接. */
-    public var heartbeatPeriodMillis: Long = 60.secondsToMillis
+    public var heartbeatPeriodMillis: Long
 
     /**
      * 状态心跳包周期. 过长会导致掉线.
@@ -138,13 +127,13 @@ public open class BotConfiguration { // open for Java
      * @since 2.6
      * @see heartbeatStrategy
      */
-    public var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
+    public var statHeartbeatPeriodMillis: Long
 
     /**
      * 心跳策略.
      * @since 2.6.3
      */
-    public var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
+    public var heartbeatStrategy: HeartbeatStrategy
 
     /**
      * 心跳策略.
@@ -178,7 +167,7 @@ public open class BotConfiguration { // open for Java
      * 每次心跳时等待结果的时间.
      * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
      */
-    public var heartbeatTimeoutMillis: Long = 5.secondsToMillis
+    public var heartbeatTimeoutMillis: Long
 
     /** 心跳失败后的第一次重连前的等待时间. */
     @Deprecated(
@@ -186,7 +175,7 @@ public 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 = 5.secondsToMillis
+    public var firstReconnectDelayMillis: Long
 
     /** 重连失败后, 继续尝试的每次等待时间 */
     @Deprecated(
@@ -194,10 +183,10 @@ public 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 = 5.secondsToMillis
+    public var reconnectPeriodMillis: Long
 
     /** 最多尝试多少次重连 */
-    public var reconnectionRetryTimes: Int = Int.MAX_VALUE
+    public var reconnectionRetryTimes: Int
 
     /**
      * 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
@@ -206,7 +195,7 @@ public open class BotConfiguration { // open for Java
      *
      * @since 2.1
      */
-    public var autoReconnectOnForceOffline: Boolean = false
+    public var autoReconnectOnForceOffline: Boolean
 
     /**
      * 验证码处理器
@@ -218,10 +207,10 @@ public open class BotConfiguration { // open for Java
      *
      * @see LoginSolver
      */
-    public var loginSolver: LoginSolver? = LoginSolver.Default
+    public var loginSolver: LoginSolver?
 
     /** 使用协议类型 */
-    public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
+    public var protocol: MiraiProtocol
 
     public enum class MiraiProtocol {
         /**
@@ -265,23 +254,21 @@ public open class BotConfiguration { // open for Java
      *
      * @since 2.2
      */
-    public var highwayUploadCoroutineCount: Int = Runtime.getRuntime().availableProcessors()
+    public var highwayUploadCoroutineCount: Int
 
     /**
      * 设置 [autoReconnectOnForceOffline] 为 `true`, 即在被挤下线时自动重连.
      * @since 2.1
      */
     @ConfigurationDsl
-    public fun autoReconnectOnForceOffline() {
-        autoReconnectOnForceOffline = true
-    }
+    public fun autoReconnectOnForceOffline()
 
     ///////////////////////////////////////////////////////////////////////////
     // Device
     ///////////////////////////////////////////////////////////////////////////
 
     @JvmField
-    internal var accountSecrets: Boolean = true
+    internal var accountSecrets: Boolean
 
     /**
      * 禁止保存 `account.secrets`.
@@ -291,16 +278,14 @@ public open class BotConfiguration { // open for Java
      *
      * @since 2.11
      */
-    public fun disableAccountSecretes() {
-        accountSecrets = false
-    }
+    public fun disableAccountSecretes()
 
     /**
      * 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
      * @see fileBasedDeviceInfo 使用指定文件存储设备信息
      * @see randomDeviceInfo 使用随机设备信息
      */
-    public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
+    public var deviceInfo: ((Bot) -> DeviceInfo)?
 
     /**
      * 使用随机设备信息.
@@ -308,9 +293,7 @@ public open class BotConfiguration { // open for Java
      * @see deviceInfo
      */
     @ConfigurationDsl
-    public fun randomDeviceInfo() {
-        deviceInfo = null
-    }
+    public fun randomDeviceInfo()
 
     /**
      * 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
@@ -318,11 +301,7 @@ public open class BotConfiguration { // open for Java
      * @see deviceInfo
      */
     @ConfigurationDsl
-    public fun loadDeviceInfoJson(json: String) {
-        deviceInfo = {
-            Companion.json.decodeFromString(DeviceInfo.serializer(), json)
-        }
-    }
+    public fun loadDeviceInfoJson(json: String)
 
     /**
      * 使用文件存储设备信息.
@@ -333,9 +312,7 @@ public open class BotConfiguration { // open for Java
      */
     @JvmOverloads
     @ConfigurationDsl
-    public fun fileBasedDeviceInfo(filepath: String = "device.json") {
-        deviceInfo = getFileBasedDeviceInfoSupplier { workingDir.resolve(filepath) }
-    }
+    public fun fileBasedDeviceInfo(filepath: String = "device.json")
 
     ///////////////////////////////////////////////////////////////////////////
     // Logging
@@ -351,9 +328,7 @@ public open class BotConfiguration { // open for Java
      *
      * @see MiraiLogger
      */
-    public var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
-    }
+    public var botLoggerSupplier: ((Bot) -> MiraiLogger)
 
     /**
      * 网络层日志构造器
@@ -365,94 +340,22 @@ public open class BotConfiguration { // open for Java
      *
      * @see MiraiLogger
      */
-    public var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
-        MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
-    }
-
-
-    /**
-     * 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
-     * 默认目录路径为 "$workingDir/logs/".
-     * @see DirectoryLogger
-     * @see redirectNetworkLogToDirectory
-     */
-    @JvmOverloads
-    @ConfigurationDsl
-    public fun redirectNetworkLogToDirectory(
-        dir: File = File("logs"),
-        retain: Long = 1.weeksToMillis,
-        identity: (bot: Bot) -> String = { "Net ${it.id}" }
-    ) {
-        require(!dir.isFile) { "dir must not be a file" }
-        networkLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) }
-    }
-
-    /**
-     * 重定向 [网络日志][networkLoggerSupplier] 到指定文件. 默认文件路径为 "$workingDir/mirai.log".
-     * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile])
-     * @see SingleFileLogger
-     * @see redirectNetworkLogToDirectory
-     */
-    @JvmOverloads
-    @ConfigurationDsl
-    public fun redirectNetworkLogToFile(
-        file: File = File("mirai.log"),
-        identity: (bot: Bot) -> String = { "Net ${it.id}" }
-    ) {
-        require(!file.isDirectory) { "file must not be a dir" }
-        networkLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) }
-    }
-
-    /**
-     * 重定向 [Bot 日志][botLoggerSupplier] 到指定文件.
-     * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile])
-     * @see SingleFileLogger
-     * @see redirectBotLogToDirectory
-     */
-    @JvmOverloads
-    @ConfigurationDsl
-    public fun redirectBotLogToFile(
-        file: File = File("mirai.log"),
-        identity: (bot: Bot) -> String = { "Bot ${it.id}" }
-    ) {
-        require(!file.isDirectory) { "file must not be a dir" }
-        botLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) }
-    }
-
-
-    /**
-     * 重定向 [Bot 日志][botLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
-     * @see DirectoryLogger
-     * @see redirectBotLogToFile
-     */
-    @JvmOverloads
-    @ConfigurationDsl
-    public fun redirectBotLogToDirectory(
-        dir: File = File("logs"),
-        retain: Long = 1.weeksToMillis,
-        identity: (bot: Bot) -> String = { "Bot ${it.id}" }
-    ) {
-        require(!dir.isFile) { "dir must not be a file" }
-        botLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) }
-    }
+    public var networkLoggerSupplier: ((Bot) -> MiraiLogger)
 
     /**
      * 不显示网络日志. 不推荐.
      * @see networkLoggerSupplier 更多日志处理方式
      */
     @ConfigurationDsl
-    public fun noNetworkLog() {
-        networkLoggerSupplier = { _ -> SilentLogger }
-    }
+    public fun noNetworkLog()
+
 
     /**
      * 不显示 [Bot] 日志. 不推荐.
      * @see botLoggerSupplier 更多日志处理方式
      */
     @ConfigurationDsl
-    public fun noBotLog() {
-        botLoggerSupplier = { _ -> SilentLogger }
-    }
+    public fun noBotLog()
 
     /**
      * 是否显示过于冗长的事件日志
@@ -461,34 +364,17 @@ public open class BotConfiguration { // open for Java
      *
      * @since 2.8
      */
-    public var isShowingVerboseEventLog: Boolean = false
+    public var isShowingVerboseEventLog: Boolean
 
     ///////////////////////////////////////////////////////////////////////////
     // Cache
     //////////////////////////////////////////////////////////////////////////
 
-    /**
-     * 缓存数据目录, 相对于 [workingDir].
-     *
-     * 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
-     * 默认启用的缓存可以加快登录过程.
-     *
-     * 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
-     * - 联系人列表
-     * - 登录服务器列表
-     * - 资源服务秘钥
-     *
-     * 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
-     *
-     * @since 2.4
-     */
-    public var cacheDir: File = File("cache")
-
     /**
      * 联系人信息缓存配置. 将会保存在 [cacheDir] 中 `contacts` 目录
      * @since 2.4
      */
-    public var contactListCache: ContactListCache = ContactListCache()
+    public var contactListCache: ContactListCache
 
     /**
      * 联系人信息缓存配置
@@ -501,26 +387,22 @@ public open class BotConfiguration { // open for Java
         /**
          * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
          */
-        public var saveIntervalMillis: Long = 60_000
+        public var saveIntervalMillis: Long
 
         /**
          * 在有修改时自动保存间隔. 默认 60 秒. 在每次登录完成后有修改时都会立即保存一次.
          */ // was @ExperimentalTime before 2.9
-        public inline var saveInterval: Duration
-            @JvmSynthetic inline get() = saveIntervalMillis.milliseconds
-            @JvmSynthetic inline set(v) {
-                saveIntervalMillis = v.inWholeMilliseconds
-            }
+        public var saveInterval: Duration
 
         /**
          * 开启好友列表缓存.
          */
-        public var friendListCacheEnabled: Boolean = false
+        public var friendListCacheEnabled: Boolean
 
         /**
          * 开启群成员列表缓存.
          */
-        public var groupMemberListCacheEnabled: Boolean = false
+        public var groupMemberListCacheEnabled: Boolean
     }
 
     /**
@@ -534,29 +416,21 @@ public open class BotConfiguration { // open for Java
      * @since 2.4
      */
     @JvmSynthetic
-    public inline fun contactListCache(action: ContactListCache.() -> Unit) {
-        action.invoke(this.contactListCache)
-    }
+    public inline fun contactListCache(action: ContactListCache.() -> Unit)
 
     /**
      * 禁用好友列表和群成员列表的缓存.
      * @since 2.4
      */
     @ConfigurationDsl
-    public fun disableContactCache() {
-        contactListCache.friendListCacheEnabled = false
-        contactListCache.groupMemberListCacheEnabled = false
-    }
+    public fun disableContactCache()
 
     /**
      * 启用好友列表和群成员列表的缓存.
      * @since 2.4
      */
     @ConfigurationDsl
-    public fun enableContactCache() {
-        contactListCache.friendListCacheEnabled = true
-        contactListCache.groupMemberListCacheEnabled = true
-    }
+    public fun enableContactCache()
 
     /**
      * 登录缓存.
@@ -570,38 +444,14 @@ public open class BotConfiguration { // open for Java
      *
      * @since 2.6
      */
-    public var loginCacheEnabled: Boolean = true
+    public var loginCacheEnabled: Boolean
 
     ///////////////////////////////////////////////////////////////////////////
     // Misc
     ///////////////////////////////////////////////////////////////////////////
 
     @Suppress("DuplicatedCode")
-    public 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
-        }
-    }
+    public fun copy(): BotConfiguration
 
     /**
      * 是否处理接受到的特殊换行符, 默认为 `true`
@@ -612,28 +462,17 @@ public open class BotConfiguration { // open for Java
      * @since 2.4
      */
     @get:JvmName("isConvertLineSeparator")
-    public var convertLineSeparator: Boolean = true
+    public var convertLineSeparator: Boolean
 
     /** 标注一个配置 DSL 函数 */
     @Target(AnnotationTarget.FUNCTION)
     @DslMarker
-    public annotation class ConfigurationDsl
+    public annotation class ConfigurationDsl()
 
     public companion object {
         /** 默认的配置实例. 可以进行修改 */
         @JvmStatic
-        public val Default: BotConfiguration = BotConfiguration()
-
-        internal val json: Json = kotlin.runCatching {
-            Json {
-                isLenient = true
-                ignoreUnknownKeys = true
-                prettyPrint = true
-            }
-        }.getOrElse {
-            @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatibility for older versions
-            Json {}
-        }
+        public val Default: BotConfiguration
     }
 }
 

+ 132 - 132
mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.utils
@@ -12,6 +12,7 @@ package net.mamoe.mirai.utils
 import kotlinx.io.core.toByteArray
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
+import kotlinx.serialization.Serializer
 import kotlinx.serialization.Transient
 import kotlinx.serialization.builtins.serializer
 import kotlinx.serialization.json.Json
@@ -21,98 +22,85 @@ import kotlinx.serialization.json.jsonPrimitive
 import kotlinx.serialization.protobuf.ProtoBuf
 import kotlinx.serialization.protobuf.ProtoNumber
 import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans
-import java.io.File
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmStatic
 import kotlin.random.Random
 
-@Serializable
-public class DeviceInfo(
-    public val display: ByteArray,
-    public val product: ByteArray,
-    public val device: ByteArray,
-    public val board: ByteArray,
-    public val brand: ByteArray,
-    public val model: ByteArray,
-    public val bootloader: ByteArray,
-    public val fingerprint: ByteArray,
-    public val bootId: ByteArray,
-    public val procVersion: ByteArray,
-    public val baseBand: ByteArray,
-    public val version: Version,
-    public val simInfo: ByteArray,
-    public val osType: ByteArray,
-    public val macAddress: ByteArray,
-    public val wifiBSSID: ByteArray,
-    public val wifiSSID: ByteArray,
-    public val imsiMd5: ByteArray,
-    public val imei: String,
-    public val apn: ByteArray
+public expect class DeviceInfo(
+    display: ByteArray,
+    product: ByteArray,
+    device: ByteArray,
+    board: ByteArray,
+    brand: ByteArray,
+    model: ByteArray,
+    bootloader: ByteArray,
+    fingerprint: ByteArray,
+    bootId: ByteArray,
+    procVersion: ByteArray,
+    baseBand: ByteArray,
+    version: Version,
+    simInfo: ByteArray,
+    osType: ByteArray,
+    macAddress: ByteArray,
+    wifiBSSID: ByteArray,
+    wifiSSID: ByteArray,
+    imsiMd5: ByteArray,
+    imei: String,
+    apn: ByteArray
 ) {
-    public val androidId: ByteArray get() = display
-    public val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
 
-    init {
-        require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." }
-    }
+    public val display: ByteArray
+    public val product: ByteArray
+    public val device: ByteArray
+    public val board: ByteArray
+    public val brand: ByteArray
+    public val model: ByteArray
+    public val bootloader: ByteArray
+    public val fingerprint: ByteArray
+    public val bootId: ByteArray
+    public val procVersion: ByteArray
+    public val baseBand: ByteArray
+    public val version: Version
+    public val simInfo: ByteArray
+    public val osType: ByteArray
+    public val macAddress: ByteArray
+    public val wifiBSSID: ByteArray
+    public val wifiSSID: ByteArray
+    public val imsiMd5: ByteArray
+    public val imei: String
+    public val apn: ByteArray
+
+    public val androidId: ByteArray
+    public val ipAddress: ByteArray
 
     @Transient
     @MiraiInternalApi
-    public val guid: ByteArray = generateGuid(androidId, macAddress)
+    public val guid: ByteArray
 
-    @Serializable
     public class Version(
-        public val incremental: ByteArray = "5891938".toByteArray(),
-        public val release: ByteArray = "10".toByteArray(),
-        public val codename: ByteArray = "REL".toByteArray(),
-        public val sdk: Int = 29
+        incremental: ByteArray = "5891938".toByteArray(),
+        release: ByteArray = "10".toByteArray(),
+        codename: ByteArray = "REL".toByteArray(),
+        sdk: Int = 29
     ) {
+        public val incremental: ByteArray
+        public val release: ByteArray
+        public val codename: ByteArray
+        public val sdk: Int
+
         /**
          * @since 2.9
          */
-        override fun equals(other: Any?): Boolean {
-            if (this === other) return true
-            if (javaClass != other?.javaClass) return false
-
-            other as Version
-
-            if (!incremental.contentEquals(other.incremental)) return false
-            if (!release.contentEquals(other.release)) return false
-            if (!codename.contentEquals(other.codename)) return false
-            if (sdk != other.sdk) return false
-
-            return true
-        }
+        override fun equals(other: Any?): Boolean
 
         /**
          * @since 2.9
          */
-        override fun hashCode(): Int {
-            var result = incremental.contentHashCode()
-            result = 31 * result + release.contentHashCode()
-            result = 31 * result + codename.contentHashCode()
-            result = 31 * result + sdk
-            return result
-        }
+        override fun hashCode(): Int
     }
 
     public companion object {
-        internal val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
-
-        /**
-         * 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
-         */
-        @JvmOverloads
-        @JvmStatic
-        @JvmName("from")
-        public fun File.loadAsDeviceInfo(
-            json: Json = DeviceInfoManager.format
-        ): DeviceInfo {
-            if (!this.exists() || this.length() == 0L) {
-                return random().also {
-                    this.writeText(DeviceInfoManager.serialize(it, json))
-                }
-            }
-            return DeviceInfoManager.deserialize(this.readText(), json)
-        }
+        internal val logger: MiraiLogger
 
         /**
          * 生成随机 [DeviceInfo]
@@ -120,7 +108,7 @@ public class DeviceInfo(
          * @since 2.0
          */
         @JvmStatic
-        public fun random(): DeviceInfo = random(Random.Default)
+        public fun random(): DeviceInfo
 
         /**
          * 使用特定随机数生成器生成 [DeviceInfo]
@@ -128,61 +116,72 @@ public class DeviceInfo(
          * @since 2.9
          */
         @JvmStatic
-        public fun random(random: Random): DeviceInfo {
-            return DeviceInfo(
-                display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
-                product = "mirai".toByteArray(),
-                device = "mirai".toByteArray(),
-                board = "mirai".toByteArray(),
-                brand = "mamoe".toByteArray(),
-                model = "mirai".toByteArray(),
-                bootloader = "unknown".toByteArray(),
-                fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${
-                    getRandomIntString(7, random)
-                }:user/release-keys".toByteArray(),
-                bootId = generateUUID(getRandomByteArray(16, random).md5()).toByteArray(),
-                procVersion = "Linux version 3.0.31-${
-                    getRandomString(8, random)
-                } (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
-                baseBand = byteArrayOf(),
-                version = Version(),
-                simInfo = "T-Mobile".toByteArray(),
-                osType = "android".toByteArray(),
-                macAddress = "02:00:00:00:00:00".toByteArray(),
-                wifiBSSID = "02:00:00:00:00:00".toByteArray(),
-                wifiSSID = "<unknown ssid>".toByteArray(),
-                imsiMd5 = getRandomByteArray(16, random).md5(),
-                imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
-                apn = "wifi".toByteArray()
-            )
-        }
-
-        /**
-         * 计算 imei 校验位
-         */
-        private fun luhn(imei: String): Int {
-            var odd = false
-            val zero = '0'
-            val sum = imei.sumOf { char ->
-                odd = !odd
-                if (odd) {
-                    char.code - zero.code
-                } else {
-                    val s = (char.code - zero.code) * 2
-                    s % 10 + s / 10
-                }
-            }
-            return (10 - sum % 10) % 10
-        }
+        public fun random(random: Random): DeviceInfo
     }
 
     /**
      * @since 2.9
      */
     @Suppress("DuplicatedCode")
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
+    override fun equals(other: Any?): Boolean
+
+    /**
+     * @since 2.9
+     */
+    override fun hashCode(): Int
+}
+
+internal object DeviceInfoCommonImpl {
+    fun randomDeviceInfo(random: Random) = DeviceInfo(
+        display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
+        product = "mirai".toByteArray(),
+        device = "mirai".toByteArray(),
+        board = "mirai".toByteArray(),
+        brand = "mamoe".toByteArray(),
+        model = "mirai".toByteArray(),
+        bootloader = "unknown".toByteArray(),
+        fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${
+            getRandomIntString(7, random)
+        }:user/release-keys".toByteArray(),
+        bootId = generateUUID(getRandomByteArray(16, random).md5()).toByteArray(),
+        procVersion = "Linux version 3.0.31-${
+            getRandomString(8, random)
+        } (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
+        baseBand = byteArrayOf(),
+        version = DeviceInfo.Version(),
+        simInfo = "T-Mobile".toByteArray(),
+        osType = "android".toByteArray(),
+        macAddress = "02:00:00:00:00:00".toByteArray(),
+        wifiBSSID = "02:00:00:00:00:00".toByteArray(),
+        wifiSSID = "<unknown ssid>".toByteArray(),
+        imsiMd5 = getRandomByteArray(16, random).md5(),
+        imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
+        apn = "wifi".toByteArray()
+    )
+
+    /**
+     * 计算 imei 校验位
+     */
+    private fun luhn(imei: String): Int {
+        var odd = false
+        val zero = '0'
+        val sum = imei.sumOf { char ->
+            odd = !odd
+            if (odd) {
+                char.code - zero.code
+            } else {
+                val s = (char.code - zero.code) * 2
+                s % 10 + s / 10
+            }
+        }
+        return (10 - sum % 10) % 10
+    }
+
+
+    @Suppress("DuplicatedCode")
+    fun equalsImpl(deviceInfo: DeviceInfo, other: Any?): Boolean = deviceInfo.run {
+        if (deviceInfo === other) return true
+        if (other !is DeviceInfo) return false
 
         other as DeviceInfo
 
@@ -211,10 +210,8 @@ public class DeviceInfo(
         return true
     }
 
-    /**
-     * @since 2.9
-     */
-    override fun hashCode(): Int {
+    @Suppress("DuplicatedCode")
+    fun hashCodeImpl(deviceInfo: DeviceInfo): Int = deviceInfo.run {
         var result = display.contentHashCode()
         result = 31 * result + product.contentHashCode()
         result = 31 * result + device.contentHashCode()
@@ -293,6 +290,9 @@ internal object DeviceInfoManager {
         val data: T
     )
 
+    @Serializer(forClass = DeviceInfo.Version::class)
+    private object DeviceInfoVersionSerializer
+
     @Serializable
     class V1(
         val display: ByteArray,
@@ -306,7 +306,7 @@ internal object DeviceInfoManager {
         val bootId: ByteArray,
         val procVersion: ByteArray,
         val baseBand: ByteArray,
-        val version: DeviceInfo.Version,
+        val version: @Serializable(DeviceInfoVersionSerializer::class) DeviceInfo.Version,
         val simInfo: ByteArray,
         val osType: ByteArray,
         val macAddress: ByteArray,
@@ -472,7 +472,7 @@ internal object DeviceInfoManager {
  * Defaults "%4;7t>;28<fc.5*6".toByteArray()
  */
 @Suppress("RemoveRedundantQualifierName") // bug
-private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
+internal fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
     (androidId + macAddress).md5()
 
 

+ 18 - 593
mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt

@@ -11,28 +11,24 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
+import kotlinx.io.core.Input
 import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
-import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.Contact.Companion.sendImage
 import net.mamoe.mirai.contact.Contact.Companion.uploadImage
-import net.mamoe.mirai.contact.FileSupported
-import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.internal.utils.*
 import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.FileMessage
 import net.mamoe.mirai.message.data.Image
-import net.mamoe.mirai.message.data.sendTo
-import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback
 import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
 import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
 import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
-import java.io.*
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
 
 
 /**
@@ -124,7 +120,7 @@ import kotlin.contracts.contract
  *
  * @see FileCacheStrategy
  */
-public interface ExternalResource : Closeable {
+public expect interface ExternalResource : Closeable {
 
     /**
      * 是否在 _使用一次_ 后自动 [close].
@@ -136,8 +132,7 @@ public interface ExternalResource : Closeable {
      * @since 2.8
      */
     @MiraiExperimentalApi
-    public val isAutoClose: Boolean
-        get() = false
+    public open val isAutoClose: Boolean
 
     /**
      * 文件内容 MD5. 16 bytes
@@ -148,11 +143,7 @@ public interface ExternalResource : Closeable {
      * 文件内容 SHA1. 16 bytes
      * @since 2.5
      */
-    public val sha1: ByteArray
-        get() =
-            throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
-    // 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
-    // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.
+    public open val sha1: ByteArray
 
 
     /**
@@ -178,17 +169,17 @@ public interface ExternalResource : Closeable {
     public val closed: Deferred<Unit>
 
     /**
-     * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流.
+     * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流.
      *
      * 关闭此流不会关闭 [ExternalResource].
      * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
+     *
+     * @since SINCE_NATIVE_TARGET
      */
-    public fun inputStream(): InputStream
+    public fun input(): Input
 
     @MiraiInternalApi
-    public fun calculateResourceId(): String {
-        return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
-    }
+    public open fun calculateResourceId(): String
 
     /**
      * 该 [ExternalResource] 的数据来源, 可能有以下的返回
@@ -209,25 +200,14 @@ public interface ExternalResource : Closeable {
      *
      * @since 2.8.0
      */
-    public val origin: Any? get() = null
+    public open val origin: Any?
 
     /**
      * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
      *
      * @since 2.8.0
      */
-    public fun toAutoCloseable(): ExternalResource {
-        return if (isAutoClose) this else {
-            val delegate = this
-            object : ExternalResource by delegate {
-                override val isAutoClose: Boolean get() = true
-                override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)"
-                override fun toAutoCloseable(): ExternalResource {
-                    return this
-                }
-            }
-        }
-    }
+    public open fun toAutoCloseable(): ExternalResource
 
 
     public companion object {
@@ -236,48 +216,12 @@ public interface ExternalResource : Closeable {
          *
          * @see ExternalResource.formatName
          */
-        public const val DEFAULT_FORMAT_NAME: String = "mirai"
+        public val DEFAULT_FORMAT_NAME: String
 
         ///////////////////////////////////////////////////////////////////////////
         // region toExternalResource
         ///////////////////////////////////////////////////////////////////////////
 
-        /**
-         * **打开文件**并创建 [ExternalResource].
-         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
-         *
-         * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close].
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         */
-        @JvmStatic
-        @JvmOverloads
-        @JvmName("create")
-        public fun File.toExternalResource(formatName: String? = null): ExternalResource =
-            // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException
-            RandomAccessFile(this, "r").toExternalResource(formatName).also {
-                it.cast<ExternalResourceImplByFile>().origin = this@toExternalResource
-            }
-
-        /**
-         * 创建 [ExternalResource].
-         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭, 届时将会关闭 [RandomAccessFile].
-         *
-         * **注意**:若关闭 [RandomAccessFile], 也会间接关闭 [ExternalResource].
-         *
-         * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会.
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         */
-        @JvmStatic
-        @JvmOverloads
-        @JvmName("create")
-        public fun RandomAccessFile.toExternalResource(
-            formatName: String? = null,
-            closeOriginalFileOnClose: Boolean = true,
-        ): ExternalResource =
-            ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose)
-
         /**
          * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
          *
@@ -286,67 +230,10 @@ public interface ExternalResource : Closeable {
         @JvmStatic
         @JvmOverloads
         @JvmName("create")
-        public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource =
-            ExternalResourceImplByByteArray(this, formatName)
-
-
-        /**
-         * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource].
-         * 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
-         *
-         * **注意**:本函数不会关闭流.
-         *
-         * ### 在 Java 获得和使用 [ExternalResource] 实例
-         *
-         * ```
-         * try(ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
-         *     contact.uploadImage(resource); // 用来上传图片
-         *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
-         * }
-         * ```
-         *
-         * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
-         *
-         * ```
-         * try(InputStream stream = ...) {
-         *     try(ExternalResource resource = ExternalResource.create(stream)) {
-         *         contact.uploadImage(resource); // 用来上传图片
-         *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
-         *     }
-         * }
-         * ```
-         *
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         * @see ExternalResource
-         */
-        @JvmStatic
-        @JvmOverloads
-        @JvmName("create")
-        @Throws(IOException::class) // not in BIO context so propagate IOException
-        public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource =
-            Mirai.FileCacheStrategy.newCache(this, formatName)
+        public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource
 
         // endregion
 
-
-        /* note:
-        于 2.8.0-M1 添加 (#1392)
-
-        于 2.8.0-RC 移动至 `toExternalResource`(#1588)
-         */
-        @JvmName("createAutoCloseable")
-        @JvmStatic
-        @Deprecated(
-            level = DeprecationLevel.HIDDEN,
-            message = "Moved to `toExternalResource()`",
-            replaceWith = ReplaceWith("resource.toAutoCloseable()"),
-        )
-        @DeprecatedSinceMirai(errorSince = "2.8", hiddenSince = "2.10")
-        public fun createAutoCloseable(resource: ExternalResource): ExternalResource {
-            return resource.toAutoCloseable()
-        }
-
         ///////////////////////////////////////////////////////////////////////////
         // region sendAsImageTo
         ///////////////////////////////////////////////////////////////////////////
@@ -364,43 +251,7 @@ public interface ExternalResource : Closeable {
         @JvmBlockingBridge
         @JvmStatic
         @JvmName("sendAsImage")
-        public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
-            contact.uploadImage(this).sendTo(contact)
-
-        /**
-         * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
-         *
-         * 注意:本函数不会关闭流.
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         * @throws OverFileSizeMaxException
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmName("sendAsImage")
-        @JvmOverloads
-        public suspend fun <C : Contact> InputStream.sendAsImageTo(
-            contact: C,
-            formatName: String? = null,
-        ): MessageReceipt<C> =
-            runBIO {
-                // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
-                toExternalResource(formatName)
-            }.withUse { sendAsImageTo(contact) }
-
-        /**
-         * 将文件作为图片发送到指定联系人.
-         * @param formatName 查看 [ExternalResource.formatName]
-         * @throws OverFileSizeMaxException
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmName("sendAsImage")
-        @JvmOverloads
-        public suspend fun <C : Contact> File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt<C> {
-            require(this.exists() && this.canRead())
-            return toExternalResource(formatName).withUse { sendAsImageTo(contact) }
-        }
+        public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C>
 
         // endregion
 
@@ -419,435 +270,9 @@ public interface ExternalResource : Closeable {
          */
         @JvmStatic
         @JvmBlockingBridge
-        public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)
-
-        /**
-         * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image].
-         *
-         * 注意:本函数不会关闭流.
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         * @throws OverFileSizeMaxException
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmOverloads
-        public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image =
-            // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
-            runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) }
-
-        // endregion
-
-        ///////////////////////////////////////////////////////////////////////////
-        // region uploadAsFile
-        ///////////////////////////////////////////////////////////////////////////
-
-        /**
-         * 将文件作为图片上传后构造 [Image].
-         *
-         * @param formatName 查看 [ExternalResource.formatName]
-         * @throws OverFileSizeMaxException
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmOverloads
-        public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image =
-            toExternalResource(formatName).withUse { uploadAsImage(contact) }
-
-        /**
-         * 上传文件并获取文件消息.
-         *
-         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
-         *
-         * 需要调用方手动[关闭资源][ExternalResource.close].
-         *
-         * ## 已弃用
-         * 查看 [RemoteFile.upload] 获取更多信息.
-         *
-         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
-         * @since 2.5
-         * @see RemoteFile.path
-         * @see RemoteFile.upload
-         */
-        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmOverloads
-        @Deprecated(
-            "Use sendTo instead.",
-            ReplaceWith(
-                "this.sendTo(contact, path, callback)",
-                "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo"
-            ),
-            level = DeprecationLevel.HIDDEN
-        ) // deprecated since 2.7-M1
-        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-        public suspend fun File.uploadTo(
-            contact: FileSupported,
-            path: String,
-            callback: RemoteFile.ProgressionCallback? = null,
-        ): FileMessage = toExternalResource().use {
-            contact.filesRoot.resolve(path).upload(it, callback)
-        }
-
-        /**
-         * 上传文件并获取文件消息.
-         *
-         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
-         *
-         * 需要调用方手动[关闭资源][ExternalResource.close].
-         *
-         * ## 已弃用
-         * 查看 [RemoteFile.upload] 获取更多信息.
-         *
-         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
-         * @since 2.5
-         * @see RemoteFile.path
-         * @see RemoteFile.upload
-         */
-        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmName("uploadAsFile")
-        @JvmOverloads
-        @Deprecated(
-            "Use sendAsFileTo instead.",
-            ReplaceWith(
-                "this.sendAsFileTo(contact, path, callback)",
-                "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo"
-            ),
-            level = DeprecationLevel.HIDDEN
-        ) // deprecated since 2.7-M1
-        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-        public suspend fun ExternalResource.uploadAsFile(
-            contact: FileSupported,
-            path: String,
-            callback: RemoteFile.ProgressionCallback? = null,
-        ): FileMessage {
-            return contact.filesRoot.resolve(path).upload(this, callback)
-        }
+        public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image
 
         // endregion
-
-        ///////////////////////////////////////////////////////////////////////////
-        // region sendAsFileTo
-        ///////////////////////////////////////////////////////////////////////////
-
-        /**
-         * 上传文件并发送文件消息.
-         *
-         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
-         *
-         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
-         * @since 2.5
-         * @see RemoteFile.path
-         * @see RemoteFile.uploadAndSend
-         */
-        @Suppress("DEPRECATION_ERROR", "DEPRECATION")
-        @Deprecated(
-            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
-            ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
-            level = DeprecationLevel.ERROR,
-        ) // deprecated since 2.8.0-RC
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmOverloads
-        @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
-        public suspend fun <C : FileSupported> File.sendTo(
-            contact: C,
-            path: String,
-            callback: RemoteFile.ProgressionCallback? = null,
-        ): MessageReceipt<C> = toExternalResource().use {
-            contact.filesRoot.resolve(path).upload(it, callback).sendTo(contact)
-        }
-
-        /**
-         * 上传文件并发送件消息.  如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
-         *
-         * 需要调用方手动[关闭资源][ExternalResource.close].
-         *
-         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
-         * @since 2.5
-         * @see RemoteFile.path
-         * @see RemoteFile.uploadAndSend
-         */
-        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-        @Deprecated(
-            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
-            ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
-            level = DeprecationLevel.ERROR,
-        ) // deprecated since 2.8.0-RC
-        @JvmStatic
-        @JvmBlockingBridge
-        @JvmName("sendAsFile")
-        @JvmOverloads
-        @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
-        public suspend fun <C : FileSupported> ExternalResource.sendAsFileTo(
-            contact: C,
-            path: String,
-            callback: RemoteFile.ProgressionCallback? = null,
-        ): MessageReceipt<C> {
-            return contact.filesRoot.resolve(path).upload(this, callback).sendTo(contact)
-        }
-
-        // endregion
-
-        ///////////////////////////////////////////////////////////////////////////
-        // region uploadAsVoice
-        ///////////////////////////////////////////////////////////////////////////
-
-        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-        @JvmBlockingBridge
-        @JvmStatic
-        @Deprecated(
-            "Use `contact.uploadAudio(resource)` instead",
-            level = DeprecationLevel.HIDDEN
-        ) // deprecated since 2.7
-        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-        public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice {
-            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-            if (contact is Group) return contact.uploadAudio(this)
-                .let { net.mamoe.mirai.message.data.Voice.fromAudio(it) }
-            else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
-        }
-        // endregion
-    }
-}
-
-/**
- * 一个实现了基本方法的外部资源
- *
- * ## 实现
- *
- * [AbstractExternalResource] 实现了大部分必要的方法,
- * 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现
- *
- * 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的
- *
- * Example:
- * ```
- * class MyCustomExternalResource: AbstractExternalResource() {
- *      override fun inputStream0(): InputStream = FileInputStream("/test.txt")
- *      override val size: Long get() = File("/test.txt").length()
- * }
- * ```
- *
- * ## 资源释放
- *
- * 如同 mirai 内置的 [ExternalResource] 实现一样,
- * [AbstractExternalResource] 也会被注册进入资源泄露监视器
- * (即意味着 [AbstractExternalResource] 也要求手动关闭)
- *
- * 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法,
- * 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放
- *
- * 对于 [ResourceCleanCallback], 有以下要求
- *
- * - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用)
- *
- * Example:
- * ```
- * class MyRes(
- *      cleanup: ResourceCleanCallback,
- *      val delegate: Closable,
- * ): AbstractExternalResource(cleanup) {
- * }
- *
- * // 错误, 该写法会导致 Resource 永远也不会被自动释放
- * lateinit var myRes: MyRes
- * val cleanup = ResourceCleanCallback {
- *      myRes.delegate.close()
- * }
- * myRes = MyRes(cleanup, fetchDelegate())
- *
- * // 正确
- * val delegate: Closable
- * val cleanup = ResourceCleanCallback {
- *      delegate.close()
- * }
- * val myRes = MyRes(cleanup, delegate)
- * ```
- *
- * @since 2.9
- *
- * @see ExternalResource
- * @see AbstractExternalResource.setResourceCleanCallback
- * @see AbstractExternalResource.registerToLeakObserver
- */
-@Suppress("MemberVisibilityCanBePrivate")
-public abstract class AbstractExternalResource
-@JvmOverloads
-public constructor(
-    displayName: String? = null,
-    cleanup: ResourceCleanCallback? = null,
-) : ExternalResource {
-
-    public constructor(
-        cleanup: ResourceCleanCallback? = null,
-    ) : this(null, cleanup)
-
-    public fun interface ResourceCleanCallback {
-        @Throws(IOException::class)
-        public fun cleanup()
-    }
-
-    override val md5: ByteArray by lazy { inputStream().md5() }
-    override val sha1: ByteArray by lazy { inputStream().sha1() }
-    override val formatName: String by lazy {
-        inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME
-    }
-
-    private val leakObserverRegistered = atomic(false)
-
-    /**
-     * 注册 [ExternalResource] 资源泄露监视器
-     *
-     * 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器
-     *
-     * 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露
-     *
-     * 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露
-     *
-     * 正确示例:
-     * ```
-     * // Kotlin
-     * public class MyResource: AbstractExternalResource() {
-     *      init {
-     *          val res: SomeResource
-     *          // 一些资源初始化
-     *          registerToLeakObserver()
-     *          setResourceCleanCallback(Releaser(res))
-     *      }
-     *
-     *      private class Releaser(
-     *          private val res: SomeResource,
-     *      ) : AbstractExternalResource.ResourceCleanCallback {
-     *          override fun cleanup() = res.close()
-     *      }
-     * }
-     *
-     * // Java
-     * public class MyResource extends AbstractExternalResource {
-     *      public MyResource() throws IOException {
-     *          SomeResource res;
-     *          // 一些资源初始化
-     *          registerToLeakObserver();
-     *          setResourceCleanCallback(new Releaser(res));
-     *      }
-     *
-     *      private static class Releaser implements ResourceCleanCallback {
-     *          private final SomeResource res;
-     *          Releaser(SomeResource res) { this.res = res; }
-     *
-     *          public void cleanup() throws IOException { res.close(); }
-     *      }
-     * }
-     * ```
-     *
-     * @see setResourceCleanCallback
-     */
-    protected fun registerToLeakObserver() {
-        // 用户自定义 AbstractExternalResource 也许会在 <init> 的时候失败
-        // 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver
-        if (leakObserverRegistered.compareAndSet(expect = false, update = true)) {
-            ExternalResourceLeakObserver.register(this, holder)
-        }
-    }
-
-    /**
-     * 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器。
-     * **仅在我知道我在干什么的前提下调用此方法**
-     *
-     * 不建议取消注册监视器, 这可能带来意外的错误
-     *
-     * @see registerToLeakObserver
-     */
-    protected fun dontRegisterLeakObserver() {
-        leakObserverRegistered.value = true
-    }
-
-    final override fun inputStream(): InputStream {
-        registerToLeakObserver()
-        return inputStream0()
-    }
-
-    protected abstract fun inputStream0(): InputStream
-
-    /**
-     * 修改 `this` 的资源释放回调。
-     * **仅在我知道我在干什么的前提下调用此方法**
-     *
-     * ```
-     * class MyRes {
-     * // region kotlin
-     *
-     *      private inner class Releaser : ResourceCleanCallback
-     *
-     *      private class NotInnerReleaser : ResourceCleanCallback
-     *
-     *      init {
-     *          // 错误, 内部类, Releaser 存在对 MyRes 的引用
-     *          setResourceCleanCallback(Releaser())
-     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器
-     *          setResourceCleanCallback(object : ResourceCleanCallback {})
-     *          // 正确, 无 inner 修饰, 等同于 java 的 private static class
-     *          setResourceCleanCallback(NotInnerReleaser(directResource))
-     *      }
-     *
-     * // endregion kotlin
-     *
-     * // region java
-     *
-     *      private class Releaser implements ResourceCleanCallback {}
-     *      private static class StaticReleaser implements ResourceCleanCallback {}
-     *
-     *      MyRes() {
-     *          // 错误, 内部类, 存在对 MyRes 的引用
-     *          setResourceCleanCallback(new Releaser());
-     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac
-     *          setResourceCleanCallback(new ResourceCleanCallback() {});
-     *          // 正确
-     *          setResourceCleanCallback(new StaticReleaser(directResource));
-     *      }
-     *
-     * // endregion java
-     * }
-     * ```
-     *
-     * @see registerToLeakObserver
-     */
-    protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) {
-        holder.cleanup = cleanup
-    }
-
-    private class UsrCustomResHolder(
-        @JvmField var cleanup: ResourceCleanCallback?,
-        private val resourceName: String,
-    ) : ExternalResourceHolder() {
-
-        override val closed: Deferred<Unit> = CompletableDeferred()
-
-        override fun closeImpl() {
-            cleanup?.cleanup()
-        }
-
-        // display on logger of ExternalResourceLeakObserver
-        override fun toString(): String = resourceName
-    }
-
-    private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString {
-        append("ExternalResourceHolder<")
-        append(this@AbstractExternalResource.javaClass.name)
-        append('@')
-        append(System.identityHashCode(this@AbstractExternalResource))
-        append('>')
-    })
-
-    final override val closed: Deferred<Unit> get() = holder.closed.also { registerToLeakObserver() }
-
-    @Throws(IOException::class)
-    final override fun close() {
-        holder.close()
     }
 }
 

+ 6 - 5
mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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("unused", "MemberVisibilityCanBePrivate")
@@ -12,6 +12,7 @@
 package net.mamoe.mirai.utils
 
 import kotlinx.coroutines.Dispatchers
+import kotlinx.io.errors.IOException
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
@@ -20,8 +21,8 @@ import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
 import net.mamoe.mirai.utils.FileCacheStrategy.MemoryCache
 import net.mamoe.mirai.utils.FileCacheStrategy.TempCache
 import java.io.File
-import java.io.IOException
 import java.io.InputStream
+import kotlin.jvm.JvmOverloads
 
 /**
  * 资源缓存策略.

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

@@ -11,9 +11,8 @@ package net.mamoe.mirai.utils
 
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.network.LoginFailedException
-import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
 import net.mamoe.mirai.utils.LoginSolver.Companion.Default
-import java.io.File
+import kotlin.jvm.JvmField
 
 /**
  * 验证码, 设备锁解决器
@@ -80,11 +79,4 @@ public expect abstract class LoginSolver() {
         public fun getDefault(): LoginSolver
     }
 
-}
-
-internal fun getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
-    return {
-        @Suppress("DEPRECATION_ERROR")
-        file().loadAsDeviceInfo(BotConfiguration.json)
-    }
 }

+ 26 - 68
mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt

@@ -7,13 +7,15 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:Suppress("unused")
 @file:JvmMultifileClass
 @file:JvmName("Utils")
 
 package net.mamoe.mirai.utils
 
-import java.util.*
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
 import kotlin.reflect.KClass
 
 /**
@@ -46,7 +48,7 @@ public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitc
  *
  * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数.
  */
-public interface MiraiLogger {
+public expect interface MiraiLogger {
 
     /**
      * 可以 service 实现的方式覆盖.
@@ -60,32 +62,16 @@ public interface MiraiLogger {
          * @param requester 请求创建 [MiraiLogger] 的对象的 class
          * @param identity 对象标记 (备注)
          */
-        public fun create(requester: KClass<*>, identity: String? = null): MiraiLogger =
-            this.create(requester.java, identity)
-
-        /**
-         * 创建 [MiraiLogger] 实例.
-         *
-         * @param requester 请求创建 [MiraiLogger] 的对象的 class
-         * @param identity 对象标记 (备注)
-         */
-        public fun create(requester: Class<*>, identity: String? = null): MiraiLogger
-
-        /**
-         * 创建 [MiraiLogger] 实例.
-         *
-         * @param requester 请求创建 [MiraiLogger] 的对象
-         */
-        public fun create(requester: KClass<*>): MiraiLogger = create(requester, null)
+        public open fun create(requester: KClass<*>, identity: String? = null): MiraiLogger
 
         /**
          * 创建 [MiraiLogger] 实例.
          *
          * @param requester 请求创建 [MiraiLogger] 的对象
          */
-        public fun create(requester: Class<*>): MiraiLogger = create(requester, null)
+        public open fun create(requester: KClass<*>): MiraiLogger
 
-        public companion object INSTANCE : Factory by loadService(Factory::class, { DefaultFactory() })
+        public companion object INSTANCE : Factory
     }
 
     public companion object {
@@ -96,7 +82,7 @@ public interface MiraiLogger {
         @MiraiExperimentalApi
         @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
         @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-        public val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
+        public val TopLevel: MiraiLogger
 
         /**
          * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
@@ -108,9 +94,8 @@ public interface MiraiLogger {
         ) // deprecated since 2.7
         @JvmStatic
         @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
-        public fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
-            DefaultFactoryOverrides.override { _, identity -> creator(identity) }
-        }
+        public fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger)
+
 
         /**
          * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
@@ -125,7 +110,7 @@ public interface MiraiLogger {
         ) // deprecated since 2.7
         @JvmStatic
         @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-        public fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity)
+        public fun create(identity: String?): MiraiLogger
     }
 
     /**
@@ -154,7 +139,7 @@ public interface MiraiLogger {
      *
      * @since 2.7
      */
-    public val isVerboseEnabled: Boolean get() = isEnabled
+    public open val isVerboseEnabled: Boolean
 
     /**
      * 当 DEBUG 级别的日志启用时返回 `true`
@@ -165,7 +150,7 @@ public interface MiraiLogger {
      *
      * @since 2.7
      */
-    public val isDebugEnabled: Boolean get() = isEnabled
+    public open val isDebugEnabled: Boolean
 
     /**
      * 当 INFO 级别的日志启用时返回 `true`
@@ -176,7 +161,7 @@ public interface MiraiLogger {
      *
      * @since 2.7
      */
-    public val isInfoEnabled: Boolean get() = isEnabled
+    public open val isInfoEnabled: Boolean
 
     /**
      * 当 WARNING 级别的日志启用时返回 `true`
@@ -187,7 +172,7 @@ public interface MiraiLogger {
      *
      * @since 2.7
      */
-    public val isWarningEnabled: Boolean get() = isEnabled
+    public open val isWarningEnabled: Boolean
 
     /**
      * 当 ERROR 级别的日志启用时返回 `true`
@@ -198,7 +183,7 @@ public interface MiraiLogger {
      *
      * @since 2.7
      */
-    public val isErrorEnabled: Boolean get() = isEnabled
+    public open val isErrorEnabled: Boolean
 
     /**
      * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用.
@@ -213,9 +198,7 @@ public interface MiraiLogger {
     @Suppress("UNUSED_PARAMETER")
     @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-    public var follower: MiraiLogger?
-        get() = null
-        set(value) {}
+    public open var follower: MiraiLogger?
 
     /**
      * 记录一个 `verbose` 级别的日志.
@@ -223,7 +206,7 @@ public interface MiraiLogger {
      */
     public fun verbose(message: String?)
 
-    public fun verbose(e: Throwable?): Unit = verbose(null, e)
+    public open fun verbose(e: Throwable?): Unit
     public fun verbose(message: String?, e: Throwable?)
 
     /**
@@ -231,7 +214,7 @@ public interface MiraiLogger {
      */
     public fun debug(message: String?)
 
-    public fun debug(e: Throwable?): Unit = debug(null, e)
+    public open fun debug(e: Throwable?): Unit
     public fun debug(message: String?, e: Throwable?)
 
 
@@ -240,7 +223,7 @@ public interface MiraiLogger {
      */
     public fun info(message: String?)
 
-    public fun info(e: Throwable?): Unit = info(null, e)
+    public open fun info(e: Throwable?): Unit
     public fun info(message: String?, e: Throwable?)
 
 
@@ -249,7 +232,7 @@ public interface MiraiLogger {
      */
     public fun warning(message: String?)
 
-    public fun warning(e: Throwable?): Unit = warning(null, e)
+    public open fun warning(e: Throwable?): Unit
     public fun warning(message: String?, e: Throwable?)
 
 
@@ -258,12 +241,11 @@ public interface MiraiLogger {
      */
     public fun error(message: String?)
 
-    public fun error(e: Throwable?): Unit = error(null, e)
+    public open fun error(e: Throwable?): Unit
     public fun error(message: String?, e: Throwable?)
 
     /** 根据优先级调用对应函数 */
-    public fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null): Unit =
-        priority.correspondingFunction(this, message, e)
+    public open fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null): Unit
 
     /**
      * 添加一个 [follower], 返回 [follower]
@@ -280,7 +262,7 @@ public interface MiraiLogger {
     @Suppress("DeprecatedCallableAddReplaceWith")
     @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
-    public operator fun <T : MiraiLogger> plus(follower: T): T = follower
+    public open operator fun <T : MiraiLogger> plus(follower: T): T
 }
 
 
@@ -558,28 +540,4 @@ public abstract class MiraiLoggerPlatformBase : MiraiLogger {
     public override operator fun <T : MiraiLogger> plus(follower: T): T {
         return follower
     }
-}
-
-internal object DefaultFactoryOverrides {
-    var override: ((requester: Class<*>, identity: String?) -> MiraiLogger)? =
-        null // 支持 LoggerAdapters 以及兼容旧版本
-
-    @JvmStatic
-    fun override(lambda: (requester: Class<*>, identity: String?) -> MiraiLogger) {
-        override = lambda
-    }
-
-    @JvmStatic
-    fun clearOverride() {
-        override = null
-    }
-}
-
-internal class DefaultFactory : MiraiLogger.Factory {
-    override fun create(requester: Class<*>, identity: String?): MiraiLogger {
-        val override = DefaultFactoryOverrides.override
-        return if (override != null) override(requester, identity) else PlatformLogger(
-            identity ?: requester.kotlin.simpleName ?: requester.simpleName
-        )
-    }
-}
+}

+ 4 - 1
mirai-core-api/src/commonMain/kotlin/utils/OverFileSizeMaxException.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -12,6 +12,9 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
 /**
  * 图片文件过大
  */ // 不要删除多平台结构, 这是 kotlin 的 bug

+ 2 - 1
mirai-core-api/src/commonMain/kotlin/utils/ProgressionCallback.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -12,6 +12,7 @@ package net.mamoe.mirai.utils
 import kotlinx.coroutines.channels.SendChannel
 import net.mamoe.mirai.contact.file.AbsoluteFile
 import net.mamoe.mirai.utils.ProgressionCallback.Companion.asProgressionCallback
+import kotlin.jvm.JvmStatic
 
 
 /**

+ 69 - 148
mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt

@@ -8,25 +8,21 @@
  */
 
 @file:Suppress("unused", "DEPRECATION")
-@file:JvmBlockingBridge
 
 package net.mamoe.mirai.utils
 
 import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.toList
-import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
 import net.mamoe.mirai.contact.Contact
 import net.mamoe.mirai.contact.FileSupported
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.FileMessage
-import net.mamoe.mirai.message.data.sendTo
-import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
 import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
 import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
-import java.io.File
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmStatic
 
 /**
  * 表示一个远程文件或目录.
@@ -97,10 +93,13 @@ import java.io.File
  * @see FileSupported
  * @since 2.5
  */
-@Deprecated("Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files", level = DeprecationLevel.WARNING) // deprecated since 2.8.0-RC
+@Deprecated(
+    "Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
+    level = DeprecationLevel.WARNING
+) // deprecated since 2.8.0-RC
 @DeprecatedSinceMirai(warningSince = "2.8")
 @NotStableForInheritance
-public interface RemoteFile {
+public expect interface RemoteFile {
     /**
      * 文件名或目录名.
      */
@@ -136,7 +135,7 @@ public interface RemoteFile {
     /**
      * 当 [RemoteFile] 表示一个目录时返回 `true`.
      */
-    public suspend fun isDirectory(): Boolean = !isFile()
+    public open suspend fun isDirectory(): Boolean
 
     /**
      * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定.
@@ -144,46 +143,64 @@ public interface RemoteFile {
     public suspend fun length(): Long
 
     public class FileInfo @MiraiInternalApi constructor(
+        name: String,
+        id: String,
+        path: String,
+        length: Long,
+        downloadTimes: Int,
+        uploaderId: Long,
+        uploadTime: Long,
+        lastModifyTime: Long,
+        sha1: ByteArray,
+        md5: ByteArray,
+    ) {
+
         /**
          * 文件或目录名.
          */
-        public val name: String,
+        public val name: String
+
         /**
          * 唯一识别标识.
          */
-        public val id: String,
+        public val id: String
+
         /**
          * 标准绝对路径.
          */
-        public val path: String,
+        public val path: String
+
         /**
          * 文件长度 (大小) bytes, 目录的 [length] 为 0.
          */
-        public val length: Long,
+        public val length: Long
+
         /**
          * 下载次数. 目录没有下载次数, 此属性总是 `0`.
          */
-        public val downloadTimes: Int,
+        public val downloadTimes: Int
+
         /**
          * 上传者 ID. 目录没有上传者, 此属性总是 `0`.
          */
-        public val uploaderId: Long,
+        public val uploaderId: Long
+
         /**
          * 上传的时间. 目录没有上传时间, 此属性总是 `0`.
          */
-        public val uploadTime: Long,
+        public val uploadTime: Long
+
         /**
          * 上次修改时间. 时间戳秒.
          */
-        public val lastModifyTime: Long,
-        public val sha1: ByteArray,
-        public val md5: ByteArray,
-    ) {
+        public val lastModifyTime: Long
+        public val sha1: ByteArray
+        public val md5: ByteArray
+
         /**
          * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile].
          */
-        public suspend fun resolveToFile(contact: FileSupported): RemoteFile =
-            contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
+        public suspend fun resolveToFile(contact: FileSupported): RemoteFile
     }
 
     /**
@@ -231,7 +248,7 @@ public interface RemoteFile {
      * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null`
      * @see resolve
      */
-    public suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
+    public open suspend fun resolveById(id: String): RemoteFile?
 
     /**
      * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
@@ -304,12 +321,7 @@ public interface RemoteFile {
         level = DeprecationLevel.ERROR
     ) // deprecated since 2.7
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
-    public suspend fun moveTo(path: String): Boolean {
-        // Impl notes:
-        // if `path` is absolute, this works as intended.
-        // if not, `resolve(path)` will be a child path from this dir and fails always.
-        return moveTo(resolve(path))
-    }
+    public open suspend fun moveTo(path: String): Boolean
 
     /**
      * 创建目录. 目录已经存在或无管理员权限时返回 `false`.
@@ -336,7 +348,7 @@ public interface RemoteFile {
     /**
      * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList].
      */
-    public suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
+    public open suspend fun listFilesCollection(): List<RemoteFile>
 
     /**
      * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
@@ -361,24 +373,24 @@ public interface RemoteFile {
         /**
          * 当上传开始时调用
          */
-        public fun onBegin(file: RemoteFile, resource: ExternalResource) {}
+        public open fun onBegin(file: RemoteFile, resource: ExternalResource)
 
         /**
          * 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
          *
          * 提示: 可通过 [ExternalResource.size] 获取文件总大小.
          */
-        public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
+        public open fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long)
 
         /**
          * 当上传成功时调用
          */
-        public fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
+        public open fun onSuccess(file: RemoteFile, resource: ExternalResource)
 
         /**
          * 当上传以异常失败时调用
          */
-        public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
+        public open fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable)
 
         public companion object {
             /**
@@ -407,21 +419,7 @@ public interface RemoteFile {
              * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
              */
             @JvmStatic
-            public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback {
-                return object : ProgressionCallback {
-                    override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
-                        trySend(downloadedSize)
-                    }
-
-                    override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
-                        if (closeOnFinish) this@asProgressionCallback.close()
-                    }
-
-                    override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
-                        if (closeOnFinish) this@asProgressionCallback.close(exception)
-                    }
-                }
-            }
+            public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback
         }
     }
 
@@ -475,36 +473,7 @@ public interface RemoteFile {
         "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
     )  // deprecated since 2.7-M1
     @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
-    public suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
-
-    /**
-     * 上传文件.
-     * ## 已弃用
-     * 阅读 [upload] 获取更多信息
-     * @see upload
-     */
-    @Suppress("DEPRECATION_ERROR")
-    @Deprecated(
-        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(file, callback)"), DeprecationLevel.ERROR
-    ) // deprecated since 2.7-M1
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
-    public suspend fun upload(
-        file: File,
-        callback: ProgressionCallback? = null,
-    ): FileMessage = file.toExternalResource().use { upload(it, callback) }
-
-    /**
-     * 上传文件.
-     * ## 已弃用
-     * 阅读 [upload] 获取更多信息
-     * @see upload
-     */
-    @Suppress("DEPRECATION_ERROR")
-    @Deprecated(
-        "Use sendFile instead.", ReplaceWith("this.uploadAndSend(file)"), DeprecationLevel.ERROR
-    ) // deprecated since 2.7-M1
-    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
-    public suspend fun upload(file: File): FileMessage = file.toExternalResource().use { upload(it) }
+    public open suspend fun upload(resource: ExternalResource): FileMessage
 
     /**
      * 上传文件并发送文件消息.
@@ -519,44 +488,43 @@ public interface RemoteFile {
     @MiraiExperimentalApi
     public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
 
-    /**
-     * 上传文件并发送文件消息.
-     * @see uploadAndSend
-     */
-    @MiraiExperimentalApi
-    public suspend fun uploadAndSend(file: File): MessageReceipt<Contact> =
-        file.toExternalResource().use { uploadAndSend(it) }
-
     /**
      * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
      */
     public suspend fun getDownloadInfo(): DownloadInfo?
 
     public class DownloadInfo @MiraiInternalApi constructor(
+        filename: String,
+        id: String,
+        path: String,
+        url: String,
+        sha1: ByteArray,
+        md5: ByteArray,
+    ) {
+
         /**
          * @see RemoteFile.name
          */
-        public val filename: String,
+        public val filename: String
+
         /**
          * @see RemoteFile.id
          */
-        public val id: String,
+        public val id: String
+
         /**
          * 标准绝对路径
          * @see RemoteFile.path
          */
-        public val path: String,
+        public val path: String
+
         /**
          * HTTP or HTTPS URL
          */
-        public val url: String,
-        public val sha1: ByteArray,
-        public val md5: ByteArray,
-    ) {
-        override fun toString(): String {
-            return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
-                    "md5=${md5.toUHexString("")})"
-        }
+        public val url: String
+        public val sha1: ByteArray
+        public val md5: ByteArray
+        override fun toString(): String
     }
 
     public companion object {
@@ -564,7 +532,7 @@ public interface RemoteFile {
          * 根目录路径
          * @see RemoteFile.path
          */
-        public const val ROOT_PATH: String = "/"
+        public val ROOT_PATH: String
 
         /**
          * 上传文件并获取文件消息, 但不发送.
@@ -591,32 +559,7 @@ public interface RemoteFile {
             path: String,
             resource: ExternalResource,
             callback: ProgressionCallback? = null,
-        ): FileMessage = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
-
-        /**
-         * 上传文件并获取文件消息, 但不发送.
-         * ## 已弃用
-         * 阅读 [uploadFile] 获取更多信息.
-         *
-         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
-         * @see RemoteFile.upload
-         */
-        @JvmStatic
-        @JvmOverloads
-        @Deprecated(
-            "Use sendFile instead.",
-            ReplaceWith(
-                "this.sendFile(path, file, callback)",
-                "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
-            ),
-            level = DeprecationLevel.ERROR
-        ) // deprecated since 2.7-M1
-        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
-        public suspend fun FileSupported.uploadFile(
-            path: String,
-            file: File,
-            callback: ProgressionCallback? = null,
-        ): FileMessage = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(file, callback)
+        ): FileMessage
 
         /**
          * 上传文件并发送文件消息到相关 [FileSupported].
@@ -635,28 +578,6 @@ public interface RemoteFile {
             path: String,
             resource: ExternalResource,
             callback: ProgressionCallback? = null,
-        ): MessageReceipt<C> =
-            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-            this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
-
-        /**
-         * 上传文件并发送文件消息到相关 [FileSupported].
-         * @see RemoteFile.uploadAndSend
-         */
-        @JvmStatic
-        @JvmOverloads
-        @Deprecated(
-            "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
-            ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
-            level = DeprecationLevel.WARNING
-        ) // deprecated since 2.8.0-RC
-        @DeprecatedSinceMirai(warningSince = "2.8")
-        public suspend fun <C : FileSupported> C.sendFile(
-            path: String,
-            file: File,
-            callback: ProgressionCallback? = null,
-        ): MessageReceipt<C> =
-            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
-            this.filesRoot.resolve(path).upload(file, callback).sendTo(this)
+        ): MessageReceipt<C>
     }
 }

+ 1 - 1
mirai-core-api/src/commonTest/kotlin/message.data/ConstrainSingleHelperTest.kt

@@ -10,7 +10,7 @@
 package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.utils.safeCast
-import org.junit.jupiter.api.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue

+ 1 - 1
mirai-core-api/src/commonTest/kotlin/message.data/LinearMessageChainImplTest.kt

@@ -11,7 +11,7 @@ package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit
 import net.mamoe.mirai.message.data.visitor.acceptChildren
-import org.junit.jupiter.api.Test
+import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertIs
 

+ 1 - 1
mirai-core-api/src/commonTest/kotlin/message.data/MessageChainImplTest.kt

@@ -9,7 +9,7 @@
 
 package net.mamoe.mirai.message.data
 
-import org.junit.jupiter.api.Test
+import kotlin.test.Test
 import kotlin.test.assertIs
 
 internal class MessageChainImplTest {

+ 1 - 1
mirai-core-api/src/commonTest/kotlin/message.data/MessageVisitorTest.kt

@@ -13,7 +13,7 @@ import net.mamoe.mirai.contact.FileSupported
 import net.mamoe.mirai.message.data.visitor.AbstractMessageVisitor
 import net.mamoe.mirai.message.data.visitor.accept
 import net.mamoe.mirai.utils.MiraiExperimentalApi
-import org.junit.jupiter.api.Test
+import kotlin.test.Test
 import kotlin.test.assertContentEquals
 
 internal class MessageVisitorTest {

+ 111 - 0
mirai-core-api/src/jvmBaseMain/kotlin/contact/announcement/Announcements.kt

@@ -0,0 +1,111 @@
+/*
+ * 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:JvmBlockingBridge
+@file:Suppress("INAPPLICABLE_JVM_NAME")
+
+package net.mamoe.mirai.contact.announcement
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.toList
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.PermissionDeniedException
+import net.mamoe.mirai.utils.ExternalResource
+import net.mamoe.mirai.utils.NotStableForInheritance
+import java.util.stream.Stream
+
+
+/**
+ * 表示一个群的公告列表 (管理器).
+ *
+ * ## 获取群公告
+ *
+ * ### 获取 [Announcements] 实例
+ *
+ * 只可以通过 [Group.announcements] 获取一个群的公告列表, 即 [Announcements] 实例.
+ *
+ * ### 获取公告列表
+ *
+ * 通过 [asFlow] 或 [asStream] 可以获取到*惰性*流, 在从流中收集数据时才会请求服务器获取数据. 通常建议在 Kotlin 使用协程的 [asFlow], 在 Java 使用 [asStream].
+ *
+ * 若要获取全部公告列表, 可使用 [toList].
+ *
+ * ## 发布群公告
+ *
+ * 查看 [Announcement]
+ *
+ * @since 2.7
+ */
+@JvmBlockingBridge
+@NotStableForInheritance
+public actual interface Announcements {
+    /**
+     * 创建一个能获取该群内所有群公告列表的 [Flow]. 在 [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
+     *
+     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] 和 [Flow] 的[收集][Flow.collect].
+     */
+    public actual suspend fun asFlow(): Flow<OnlineAnnouncement>
+
+    /**
+     * 创建一个能获取该群内所有群公告列表的 [Stream]. 在 [Stream] 被使用时才会分页下载 [OnlineAnnouncement].
+     *
+     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] 和 [Stream] 的[收集][Stream.collect].
+     *
+     * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [asFlow].
+     */
+    public fun asStream(): Stream<OnlineAnnouncement>
+
+    /**
+     * 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
+     *
+     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取并返回已经成功获取到的 [OfflineAnnouncement] 列表.
+     *
+     * @return 此时刻的群公告只读列表.
+     */
+    public actual suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList()
+
+
+    /**
+     * 删除一条群公告. 需要管理员权限. 使用 [OnlineAnnouncement.delete] 与此方法效果相同.
+     *
+     * @param fid 公告的 [OnlineAnnouncement.fid]
+     * @return 成功返回 `true`, 群公告不存在时返回 `false`
+     *
+     * @throws PermissionDeniedException 当没有权限时抛出
+     * @throws IllegalStateException 当协议异常时抛出
+     *
+     * @see OnlineAnnouncement.delete
+     */
+    public actual suspend fun delete(fid: String): Boolean
+
+    /**
+     * 获取一条群公告.
+     * @param fid 公告的 [OnlineAnnouncement.fid]
+     * @return 返回 `null` 表示不存在该 [fid] 的群公告
+     * @throws IllegalStateException 当协议异常时抛出
+     */
+    public actual suspend fun get(fid: String): OnlineAnnouncement?
+
+    /**
+     * 在该群发布群公告并获得 [OnlineAnnouncement], 需要管理员权限. 发布公告后群内将会出现 "有新公告" 系统提示.
+     * @throws PermissionDeniedException 当没有权限时抛出
+     * @throws IllegalStateException 当协议异常时抛出
+     * @see Announcement.publishTo
+     */
+    public actual suspend fun publish(announcement: Announcement): OnlineAnnouncement
+
+    /**
+     * 上传资源作为群公告图片. 返回值可用于 [AnnouncementParameters.image].
+     *
+     * **注意**: 需要由调用方[关闭][ExternalResource.close] [resource].
+     * @throws IllegalStateException 当协议异常时抛出
+     */
+    public actual suspend fun uploadImage(resource: ExternalResource): AnnouncementImage
+}

+ 11 - 0
mirai-core-api/src/jvmBaseMain/kotlin/contact/package.kt

@@ -0,0 +1,11 @@
+/*
+ * 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.contact
+

+ 8 - 0
mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt → mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceImpls.kt

@@ -12,6 +12,8 @@ package net.mamoe.mirai.internal.utils
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
+import kotlinx.io.core.Input
+import kotlinx.io.streams.asInput
 import net.mamoe.mirai.utils.*
 import java.io.Closeable
 import java.io.InputStream
@@ -96,6 +98,8 @@ internal abstract class ExternalResourceHolder : Closeable {
 
 internal interface ExternalResourceInternal : ExternalResource {
     val holder: ExternalResourceHolder
+
+    override fun input(): Input = inputStream().asInput()
 }
 
 internal class ExternalResourceImplByFile(
@@ -157,6 +161,10 @@ internal class ExternalResourceImplByByteArray(
         get() = data//.clone()
 
     override fun inputStream(): InputStream = data.inputStream()
+    override fun input(): Input {
+        return data.inputStream().asInput()
+    }
+
     override fun close() {
         kotlin.runCatching { closed.complete(Unit) }
     }

+ 10 - 0
mirai-core-api/src/jvmBaseMain/kotlin/package.kt

@@ -0,0 +1,10 @@
+/*
+ * 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

+ 39 - 0
mirai-core-api/src/jvmBaseMain/kotlin/spi/SPIServiceLoader.kt

@@ -0,0 +1,39 @@
+/*
+ * 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.spi
+
+import net.mamoe.mirai.utils.MiraiLogger
+import java.util.*
+import kotlin.reflect.KClass
+
+
+internal actual class SPIServiceLoader<T : BaseService> actual constructor(
+    private val defaultService: T,
+    private val serviceType: KClass<T>
+) {
+    actual var service: T = defaultService
+
+    actual fun reload() {
+        val loader = ServiceLoader.load(serviceType.java)
+        service = loader.minByOrNull { it.priority } ?: defaultService
+    }
+
+
+    init {
+        reload()
+    }
+
+    actual companion object {
+        actual val SPI_SERVICE_LOADER_LOGGER: MiraiLogger by lazy {
+            MiraiLogger.Factory.create(SPIServiceLoader::class, "spi-service-loader")
+        }
+    }
+
+}

+ 257 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/AbstractExternalResource.kt

@@ -0,0 +1,257 @@
+/*
+ * 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.utils
+
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.io.errors.IOException
+import net.mamoe.mirai.internal.utils.ExternalResourceHolder
+import net.mamoe.mirai.internal.utils.ExternalResourceLeakObserver
+import net.mamoe.mirai.internal.utils.detectFileTypeAndClose
+import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback
+import java.io.InputStream
+
+/**
+ * 一个实现了基本方法的外部资源
+ *
+ * ## 实现
+ *
+ * [AbstractExternalResource] 实现了大部分必要的方法,
+ * 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现
+ *
+ * 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的
+ *
+ * Example:
+ * ```
+ * class MyCustomExternalResource: AbstractExternalResource() {
+ *      override fun inputStream0(): InputStream = FileInputStream("/test.txt")
+ *      override val size: Long get() = File("/test.txt").length()
+ * }
+ * ```
+ *
+ * ## 资源释放
+ *
+ * 如同 mirai 内置的 [ExternalResource] 实现一样,
+ * [AbstractExternalResource] 也会被注册进入资源泄露监视器
+ * (即意味着 [AbstractExternalResource] 也要求手动关闭)
+ *
+ * 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法,
+ * 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放
+ *
+ * 对于 [ResourceCleanCallback], 有以下要求
+ *
+ * - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用)
+ *
+ * Example:
+ * ```
+ * class MyRes(
+ *      cleanup: ResourceCleanCallback,
+ *      val delegate: Closable,
+ * ): AbstractExternalResource(cleanup) {
+ * }
+ *
+ * // 错误, 该写法会导致 Resource 永远也不会被自动释放
+ * lateinit var myRes: MyRes
+ * val cleanup = ResourceCleanCallback {
+ *      myRes.delegate.close()
+ * }
+ * myRes = MyRes(cleanup, fetchDelegate())
+ *
+ * // 正确
+ * val delegate: Closable
+ * val cleanup = ResourceCleanCallback {
+ *      delegate.close()
+ * }
+ * val myRes = MyRes(cleanup, delegate)
+ * ```
+ *
+ * @since 2.9
+ *
+ * @see ExternalResource
+ * @see AbstractExternalResource.setResourceCleanCallback
+ * @see AbstractExternalResource.registerToLeakObserver
+ */
+@Suppress("MemberVisibilityCanBePrivate")
+public abstract class AbstractExternalResource
+@JvmOverloads
+public constructor(
+    displayName: String? = null,
+    cleanup: ResourceCleanCallback? = null,
+) : ExternalResource {
+
+    public constructor(
+        cleanup: ResourceCleanCallback? = null,
+    ) : this(null, cleanup)
+
+    public fun interface ResourceCleanCallback {
+        @Throws(IOException::class)
+        public fun cleanup()
+    }
+
+    override val md5: ByteArray by lazy { inputStream().md5() }
+    override val sha1: ByteArray by lazy { inputStream().sha1() }
+    override val formatName: String by lazy {
+        inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME
+    }
+
+    private val leakObserverRegistered = atomic(false)
+
+    /**
+     * 注册 [ExternalResource] 资源泄露监视器
+     *
+     * 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器
+     *
+     * 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露
+     *
+     * 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露
+     *
+     * 正确示例:
+     * ```
+     * // Kotlin
+     * public class MyResource: AbstractExternalResource() {
+     *      init {
+     *          val res: SomeResource
+     *          // 一些资源初始化
+     *          registerToLeakObserver()
+     *          setResourceCleanCallback(Releaser(res))
+     *      }
+     *
+     *      private class Releaser(
+     *          private val res: SomeResource,
+     *      ) : AbstractExternalResource.ResourceCleanCallback {
+     *          override fun cleanup() = res.close()
+     *      }
+     * }
+     *
+     * // Java
+     * public class MyResource extends AbstractExternalResource {
+     *      public MyResource() throws IOException {
+     *          SomeResource res;
+     *          // 一些资源初始化
+     *          registerToLeakObserver();
+     *          setResourceCleanCallback(new Releaser(res));
+     *      }
+     *
+     *      private static class Releaser implements ResourceCleanCallback {
+     *          private final SomeResource res;
+     *          Releaser(SomeResource res) { this.res = res; }
+     *
+     *          public void cleanup() throws IOException { res.close(); }
+     *      }
+     * }
+     * ```
+     *
+     * @see setResourceCleanCallback
+     */
+    protected fun registerToLeakObserver() {
+        // 用户自定义 AbstractExternalResource 也许会在 <init> 的时候失败
+        // 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver
+        if (leakObserverRegistered.compareAndSet(expect = false, update = true)) {
+            ExternalResourceLeakObserver.register(this, holder)
+        }
+    }
+
+    /**
+     * 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器。
+     * **仅在我知道我在干什么的前提下调用此方法**
+     *
+     * 不建议取消注册监视器, 这可能带来意外的错误
+     *
+     * @see registerToLeakObserver
+     */
+    protected fun dontRegisterLeakObserver() {
+        leakObserverRegistered.value = true
+    }
+
+    final override fun inputStream(): InputStream {
+        registerToLeakObserver()
+        return inputStream0()
+    }
+
+    protected abstract fun inputStream0(): InputStream
+
+    /**
+     * 修改 `this` 的资源释放回调。
+     * **仅在我知道我在干什么的前提下调用此方法**
+     *
+     * ```
+     * class MyRes {
+     * // region kotlin
+     *
+     *      private inner class Releaser : ResourceCleanCallback
+     *
+     *      private class NotInnerReleaser : ResourceCleanCallback
+     *
+     *      init {
+     *          // 错误, 内部类, Releaser 存在对 MyRes 的引用
+     *          setResourceCleanCallback(Releaser())
+     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器
+     *          setResourceCleanCallback(object : ResourceCleanCallback {})
+     *          // 正确, 无 inner 修饰, 等同于 java 的 private static class
+     *          setResourceCleanCallback(NotInnerReleaser(directResource))
+     *      }
+     *
+     * // endregion kotlin
+     *
+     * // region java
+     *
+     *      private class Releaser implements ResourceCleanCallback {}
+     *      private static class StaticReleaser implements ResourceCleanCallback {}
+     *
+     *      MyRes() {
+     *          // 错误, 内部类, 存在对 MyRes 的引用
+     *          setResourceCleanCallback(new Releaser());
+     *          // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac
+     *          setResourceCleanCallback(new ResourceCleanCallback() {});
+     *          // 正确
+     *          setResourceCleanCallback(new StaticReleaser(directResource));
+     *      }
+     *
+     * // endregion java
+     * }
+     * ```
+     *
+     * @see registerToLeakObserver
+     */
+    protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) {
+        holder.cleanup = cleanup
+    }
+
+    private class UsrCustomResHolder(
+        @JvmField var cleanup: ResourceCleanCallback?,
+        private val resourceName: String,
+    ) : ExternalResourceHolder() {
+
+        override val closed: Deferred<Unit> = CompletableDeferred()
+
+        override fun closeImpl() {
+            cleanup?.cleanup()
+        }
+
+        // display on logger of ExternalResourceLeakObserver
+        override fun toString(): String = resourceName
+    }
+
+    private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString {
+        append("ExternalResourceHolder<")
+        append(this@AbstractExternalResource.javaClass.name)
+        append('@')
+        append(System.identityHashCode(this@AbstractExternalResource))
+        append('>')
+    })
+
+    final override val closed: Deferred<Unit> get() = holder.closed.also { registerToLeakObserver() }
+
+    @Throws(IOException::class)
+    final override fun close() {
+        holder.close()
+    }
+}

+ 656 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt

@@ -0,0 +1,656 @@
+/*
+ * 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("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()
+ *     ...
+ * }})
+ * ```
+ */
+@Suppress("PropertyName")
+public actual open class BotConfiguration { // open for Java
+    /**
+     * 工作目录. 默认为 "."
+     */
+    public var workingDir: File = File(".")
+
+    /**
+     * Json 序列化器, 使用 'kotlinx.serialization'
+     */
+    @MiraiExperimentalApi
+    @Deprecated(
+        "Changing serial format is going to be forbidden. Deprecated for removal. ",
+        level = DeprecationLevel.ERROR
+    )
+    @DeprecatedSinceMirai(errorSince = "2.11") // was experimental
+    public var json: Json = kotlin.runCatching {
+        Json {
+            isLenient = true
+            ignoreUnknownKeys = true
+            prettyPrint = true
+        }
+    }.getOrElse {
+        @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
+        Json {}
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // 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;
+    }
+
+    /**
+     * 每次心跳时等待结果的时间.
+     * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 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
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // 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 = {
+            this.json.decodeFromString(DeviceInfo.serializer(), json)
+        }
+    }
+
+    /**
+     * 使用文件存储设备信息.
+     *
+     * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常.
+     * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
+     * @see deviceInfo
+     */
+    @JvmOverloads
+    @ConfigurationDsl
+    public actual fun fileBasedDeviceInfo(filepath: String) {
+        deviceInfo = getFileBasedDeviceInfoSupplier { workingDir.resolve(filepath) }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // 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])
+     * 默认目录路径为 "$workingDir/logs/".
+     * @see DirectoryLogger
+     * @see redirectNetworkLogToDirectory
+     */
+    @JvmOverloads
+    @ConfigurationDsl
+    public fun redirectNetworkLogToDirectory(
+        dir: File = File("logs"),
+        retain: Long = 1.weeksToMillis,
+        identity: (bot: Bot) -> String = { "Net ${it.id}" }
+    ) {
+        require(!dir.isFile) { "dir must not be a file" }
+        networkLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) }
+    }
+
+    /**
+     * 重定向 [网络日志][networkLoggerSupplier] 到指定文件. 默认文件路径为 "$workingDir/mirai.log".
+     * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile])
+     * @see SingleFileLogger
+     * @see redirectNetworkLogToDirectory
+     */
+    @JvmOverloads
+    @ConfigurationDsl
+    public fun redirectNetworkLogToFile(
+        file: File = File("mirai.log"),
+        identity: (bot: Bot) -> String = { "Net ${it.id}" }
+    ) {
+        require(!file.isDirectory) { "file must not be a dir" }
+        networkLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) }
+    }
+
+    /**
+     * 重定向 [Bot 日志][botLoggerSupplier] 到指定文件.
+     * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile])
+     * @see SingleFileLogger
+     * @see redirectBotLogToDirectory
+     */
+    @JvmOverloads
+    @ConfigurationDsl
+    public fun redirectBotLogToFile(
+        file: File = File("mirai.log"),
+        identity: (bot: Bot) -> String = { "Bot ${it.id}" }
+    ) {
+        require(!file.isDirectory) { "file must not be a dir" }
+        botLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) }
+    }
+
+
+    /**
+     * 重定向 [Bot 日志][botLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
+     * @see DirectoryLogger
+     * @see redirectBotLogToFile
+     */
+    @JvmOverloads
+    @ConfigurationDsl
+    public fun redirectBotLogToDirectory(
+        dir: File = File("logs"),
+        retain: Long = 1.weeksToMillis,
+        identity: (bot: Bot) -> String = { "Bot ${it.id}" }
+    ) {
+        require(!dir.isFile) { "dir must not be a file" }
+        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
+    //////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 缓存数据目录, 相对于 [workingDir].
+     *
+     * 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
+     * 默认启用的缓存可以加快登录过程.
+     *
+     * 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
+     * - 联系人列表
+     * - 登录服务器列表
+     * - 资源服务秘钥
+     *
+     * 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
+     *
+     * @since 2.4
+     */
+    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.json = json
+            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()
+    }
+}
+
+internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
+    return {
+        @Suppress("DEPRECATION_ERROR")
+        file().loadAsDeviceInfo(json)
+    }
+}

+ 142 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/DeviceInfo.kt

@@ -0,0 +1,142 @@
+/*
+ * 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.utils
+
+import kotlinx.io.core.toByteArray
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.json.Json
+import java.io.File
+import kotlin.random.Random
+
+@Serializable
+public actual class DeviceInfo actual constructor(
+    public actual val display: ByteArray,
+    public actual val product: ByteArray,
+    public actual val device: ByteArray,
+    public actual val board: ByteArray,
+    public actual val brand: ByteArray,
+    public actual val model: ByteArray,
+    public actual val bootloader: ByteArray,
+    public actual val fingerprint: ByteArray,
+    public actual val bootId: ByteArray,
+    public actual val procVersion: ByteArray,
+    public actual val baseBand: ByteArray,
+    public actual val version: Version,
+    public actual val simInfo: ByteArray,
+    public actual val osType: ByteArray,
+    public actual val macAddress: ByteArray,
+    public actual val wifiBSSID: ByteArray,
+    public actual val wifiSSID: ByteArray,
+    public actual val imsiMd5: ByteArray,
+    public actual val imei: String,
+    public actual val apn: ByteArray
+) {
+    public actual val androidId: ByteArray get() = display
+    public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
+
+    init {
+        require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." }
+    }
+
+    @Transient
+    @MiraiInternalApi
+    public actual val guid: ByteArray = generateGuid(androidId, macAddress)
+
+    @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // serializable
+    @Serializable
+    public actual class Version actual constructor(
+        public actual val incremental: ByteArray = "5891938".toByteArray(),
+        public actual val release: ByteArray = "10".toByteArray(),
+        public actual val codename: ByteArray = "REL".toByteArray(),
+        public actual val sdk: Int = 29
+    ) {
+        /**
+         * @since 2.9
+         */
+        actual override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Version) return false
+
+            if (!incremental.contentEquals(other.incremental)) return false
+            if (!release.contentEquals(other.release)) return false
+            if (!codename.contentEquals(other.codename)) return false
+            if (sdk != other.sdk) return false
+
+            return true
+        }
+
+        /**
+         * @since 2.9
+         */
+        actual override fun hashCode(): Int {
+            var result = incremental.contentHashCode()
+            result = 31 * result + release.contentHashCode()
+            result = 31 * result + codename.contentHashCode()
+            result = 31 * result + sdk
+            return result
+        }
+    }
+
+    public actual companion object {
+        internal actual val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
+
+        /**
+         * 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
+         */
+        @JvmOverloads
+        @JvmStatic
+        @JvmName("from")
+        public fun File.loadAsDeviceInfo(
+            json: Json = DeviceInfoManager.format
+        ): DeviceInfo {
+            if (!this.exists() || this.length() == 0L) {
+                return random().also {
+                    this.writeText(DeviceInfoManager.serialize(it, json))
+                }
+            }
+            return DeviceInfoManager.deserialize(this.readText(), json)
+        }
+
+        /**
+         * 生成随机 [DeviceInfo]
+         *
+         * @since 2.0
+         */
+        @JvmStatic
+        public actual fun random(): DeviceInfo = random(Random.Default)
+
+        /**
+         * 使用特定随机数生成器生成 [DeviceInfo]
+         *
+         * @since 2.9
+         */
+        @JvmStatic
+        public actual fun random(random: Random): DeviceInfo {
+            return DeviceInfoCommonImpl.randomDeviceInfo(random)
+        }
+    }
+
+    /**
+     * @since 2.9
+     */
+    @Suppress("DuplicatedCode")
+    actual override fun equals(other: Any?): Boolean {
+        return DeviceInfoCommonImpl.equalsImpl(this, other)
+    }
+
+
+    /**
+     * @since 2.9
+     */
+    actual override fun hashCode(): Int {
+        return DeviceInfoCommonImpl.hashCodeImpl(this)
+    }
+}

+ 623 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/ExternalResource.kt

@@ -0,0 +1,623 @@
+/*
+ * 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.utils
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.io.core.Input
+import kotlinx.io.errors.IOException
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
+import net.mamoe.mirai.Mirai
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Contact.Companion.sendImage
+import net.mamoe.mirai.contact.Contact.Companion.uploadImage
+import net.mamoe.mirai.contact.FileSupported
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray
+import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile
+import net.mamoe.mirai.internal.utils.inputStream
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.FileMessage
+import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.message.data.toVoice
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+import java.io.File
+import java.io.InputStream
+import java.io.RandomAccessFile
+
+/**
+ * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
+ *
+ * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
+ *
+ * # 创建
+ * - [File.toExternalResource]
+ * - [RandomAccessFile.toExternalResource]
+ * - [ByteArray.toExternalResource]
+ * - [InputStream.toExternalResource]
+ *
+ * ## 在 Kotlin 获得和使用 [ExternalResource] 实例
+ *
+ * ```
+ * file.toExternalResource().use { resource -> // 安全地使用资源
+ *     contact.uploadImage(resource) // 用来上传图片
+ *     contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
+ * }
+ * ```
+ *
+ * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
+ *
+ * ```
+ * inputStream.use { input -> // 安全地使用 InputStream
+ *     input.toExternalResource().use { resource -> // 安全地使用资源
+ *         contact.uploadImage(resource) // 用来上传图片
+ *         contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
+ *     }
+ * }
+ * ```
+ *
+ * ## 在 Java 获得和使用 [ExternalResource] 实例
+ *
+ * ```
+ * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
+ *     contact.uploadImage(resource); // 用来上传图片
+ *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+ * }
+ * ```
+ *
+ * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
+ *
+ * ```java
+ * try (InputStream stream = ...) { // 安全地使用 InputStream
+ *     try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
+ *         contact.uploadImage(resource); // 用来上传图片
+ *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+ *     }
+ * }
+ * ```
+ *
+ * # 释放
+ *
+ * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]).
+ * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close].
+ *
+ * ## 未释放资源的补救策略
+ *
+ * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放.
+ * 这依赖于 JVM 垃圾收集策略, 因此不可靠, 资源仍然需要手动 close.
+ *
+ * ## 使用单次自动释放
+ *
+ * 若创建的资源仅需要*很快地*使用一次, 可使用 [toAutoCloseable] 获得在使用一次后就会自动关闭的资源.
+ *
+ * 示例:
+ * ```java
+ * contact.uploadImage(ExternalResource.create(file).toAutoCloseable()); // 创建并立即使用单次自动释放的资源
+ * ```
+ *
+ * **注意**: 如果仅使用 [toAutoCloseable] 而不通过 [Contact.uploadImage] 等 mirai 内置方法使用资源, 资源仍然会处于打开状态且不会被自动关闭.
+ * 最终资源会由上述*未释放资源的补救策略*关闭, 但这依赖于 JVM 垃圾收集策略而不可靠.
+ * 因此建议在创建单次自动释放的资源后就尽快使用它, 否则仍然需要考虑在正确的时间及时关闭资源.
+ *
+ * # 实现 [ExternalResource]
+ *
+ * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用.
+ *
+ * 建议继承 [AbstractExternalResource], 这将支持上文提到的资源自动释放功能.
+ *
+ * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值.
+ *
+ * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
+ * @see ExternalResource.sendAsImageTo 将资源作为图片发送
+ * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
+ * @see Contact.sendImage 发送一个资源作为图片
+ *
+ * @see FileCacheStrategy
+ */
+public actual interface ExternalResource : Closeable {
+
+    /**
+     * 是否在 _使用一次_ 后自动 [close].
+     *
+     * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功.
+     *
+     * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为.
+     *
+     * @since 2.8
+     */
+    @MiraiExperimentalApi
+    public actual val isAutoClose: Boolean
+        get() = false
+
+    /**
+     * 文件内容 MD5. 16 bytes
+     */
+    public actual val md5: ByteArray
+
+    /**
+     * 文件内容 SHA1. 16 bytes
+     * @since 2.5
+     */
+    public actual val sha1: ByteArray
+        get() =
+            throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
+    // 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
+    // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.
+
+
+    /**
+     * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME].
+     *
+     * 默认会从文件头识别, 支持的文件类型:
+     * png, jpg, gif, tif, bmp, amr, silk
+     *
+     * @see net.mamoe.mirai.utils.getFileType
+     * @see net.mamoe.mirai.utils.FILE_TYPES
+     * @see DEFAULT_FORMAT_NAME
+     */
+    public actual val formatName: String
+
+    /**
+     * 文件大小 bytes
+     */
+    public actual val size: Long
+
+    /**
+     * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred].
+     */
+    public actual val closed: Deferred<Unit>
+
+    /**
+     * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流.
+     *
+     * 关闭此流不会关闭 [ExternalResource].
+     * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
+     */
+    public fun inputStream(): InputStream
+
+    /**
+     * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流.
+     *
+     * 关闭此流不会关闭 [ExternalResource].
+     * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
+     *
+     * @since SINCE_NATIVE_TARGET
+     */
+    @MiraiExperimentalApi
+    public actual fun input(): Input
+
+    @MiraiInternalApi
+    public actual fun calculateResourceId(): String {
+        return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
+    }
+
+    /**
+     * 该 [ExternalResource] 的数据来源, 可能有以下的返回
+     *
+     * - [File] 本地文件
+     * - [java.nio.file.Path] 某个具体文件路径
+     * - [java.nio.ByteBuffer] RAM
+     * - [java.net.URI] uri
+     * - [ByteArray] RAM
+     * - Or more...
+     *
+     * implementation note:
+     *
+     * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null`
+     * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型)
+     * - 不要返回 [String], 没有约定 [String] 代表什么
+     * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile])
+     *
+     * @since 2.8.0
+     */
+    public actual val origin: Any? get() = null
+
+    /**
+     * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
+     *
+     * @since 2.8.0
+     */
+    public actual fun toAutoCloseable(): ExternalResource {
+        return if (isAutoClose) this else {
+            val delegate = this
+            object : ExternalResource by delegate {
+                override val isAutoClose: Boolean get() = true
+                override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)"
+                override fun toAutoCloseable(): ExternalResource {
+                    return this
+                }
+            }
+        }
+    }
+
+
+    public actual companion object {
+        /**
+         * 在无法识别文件格式时使用的默认格式名. "mirai".
+         *
+         * @see ExternalResource.formatName
+         */
+        public actual const val DEFAULT_FORMAT_NAME: String = "mirai"
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region toExternalResource
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * **打开文件**并创建 [ExternalResource].
+         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
+         *
+         * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close].
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public fun File.toExternalResource(formatName: String? = null): ExternalResource =
+            // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException
+            RandomAccessFile(this, "r").toExternalResource(formatName).also {
+                it.cast<ExternalResourceImplByFile>().origin = this@toExternalResource
+            }
+
+        /**
+         * 创建 [ExternalResource].
+         * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭, 届时将会关闭 [RandomAccessFile].
+         *
+         * **注意**:若关闭 [RandomAccessFile], 也会间接关闭 [ExternalResource].
+         *
+         * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会.
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public fun RandomAccessFile.toExternalResource(
+            formatName: String? = null,
+            closeOriginalFileOnClose: Boolean = true,
+        ): ExternalResource =
+            ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose)
+
+        /**
+         * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        public actual fun ByteArray.toExternalResource(formatName: String?): ExternalResource =
+            ExternalResourceImplByByteArray(this, formatName)
+
+
+        /**
+         * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource].
+         * 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
+         *
+         * **注意**:本函数不会关闭流.
+         *
+         * ### 在 Java 获得和使用 [ExternalResource] 实例
+         *
+         * ```
+         * try(ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
+         *     contact.uploadImage(resource); // 用来上传图片
+         *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+         * }
+         * ```
+         *
+         * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
+         *
+         * ```
+         * try(InputStream stream = ...) {
+         *     try(ExternalResource resource = ExternalResource.create(stream)) {
+         *         contact.uploadImage(resource); // 用来上传图片
+         *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+         *     }
+         * }
+         * ```
+         *
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         * @see ExternalResource
+         */
+        @JvmStatic
+        @JvmOverloads
+        @JvmName("create")
+        @Throws(IOException::class) // not in BIO context so propagate IOException
+        public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource =
+            Mirai.FileCacheStrategy.newCache(this, formatName)
+
+        // endregion
+
+
+        /* note:
+        于 2.8.0-M1 添加 (#1392)
+
+        于 2.8.0-RC 移动至 `toExternalResource`(#1588)
+         */
+        @JvmName("createAutoCloseable")
+        @JvmStatic
+        @Deprecated(
+            level = DeprecationLevel.HIDDEN,
+            message = "Moved to `toExternalResource()`",
+            replaceWith = ReplaceWith("resource.toAutoCloseable()"),
+        )
+        @DeprecatedSinceMirai(errorSince = "2.8", hiddenSince = "2.10")
+        public fun createAutoCloseable(resource: ExternalResource): ExternalResource {
+            return resource.toAutoCloseable()
+        }
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region sendAsImageTo
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 将图片作为单独的消息发送给指定联系人.
+         *
+         * **注意**:本函数不会关闭 [ExternalResource].
+         *
+         * @see Contact.uploadImage 上传图片
+         * @see Contact.sendMessage 最终调用, 发送消息.
+         *
+         * @throws OverFileSizeMaxException
+         */
+        @JvmBlockingBridge
+        @JvmStatic
+        @JvmName("sendAsImage")
+        public actual suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
+            contact.uploadImage(this).sendTo(contact)
+
+        /**
+         * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
+         *
+         * 注意:本函数不会关闭流.
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         * @throws OverFileSizeMaxException
+         */
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmName("sendAsImage")
+        @JvmOverloads
+        public suspend fun <C : Contact> InputStream.sendAsImageTo(
+            contact: C,
+            formatName: String? = null,
+        ): MessageReceipt<C> =
+            runBIO {
+                // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
+                toExternalResource(formatName)
+            }.withUse { sendAsImageTo(contact) }
+
+        /**
+         * 将文件作为图片发送到指定联系人.
+         * @param formatName 查看 [ExternalResource.formatName]
+         * @throws OverFileSizeMaxException
+         */
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmName("sendAsImage")
+        @JvmOverloads
+        public suspend fun <C : Contact> File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt<C> {
+            require(this.exists() && this.canRead())
+            return toExternalResource(formatName).withUse { sendAsImageTo(contact) }
+        }
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region uploadAsImage
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
+         *
+         * **注意**:本函数不会关闭 [ExternalResource].
+         *
+         * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
+         *
+         * @see Contact.uploadImage 最终调用, 上传图片.
+         */
+        @JvmStatic
+        @JvmBlockingBridge
+        public actual suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)
+
+        /**
+         * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image].
+         *
+         * 注意:本函数不会关闭流.
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         * @throws OverFileSizeMaxException
+         */
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmOverloads
+        public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image =
+            // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo
+            runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) }
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region uploadAsFile
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 将文件作为图片上传后构造 [Image].
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         * @throws OverFileSizeMaxException
+         */
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmOverloads
+        public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image =
+            toExternalResource(formatName).withUse { uploadAsImage(contact) }
+
+        /**
+         * 上传文件并获取文件消息.
+         *
+         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
+         *
+         * 需要调用方手动[关闭资源][ExternalResource.close].
+         *
+         * ## 已弃用
+         * 查看 [RemoteFile.upload] 获取更多信息.
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @since 2.5
+         * @see RemoteFile.path
+         * @see RemoteFile.upload
+         */
+        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmOverloads
+        @Deprecated(
+            "Use sendTo instead.",
+            ReplaceWith(
+                "this.sendTo(contact, path, callback)",
+                "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo"
+            ),
+            level = DeprecationLevel.HIDDEN
+        ) // deprecated since 2.7-M1
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public suspend fun File.uploadTo(
+            contact: FileSupported,
+            path: String,
+            callback: RemoteFile.ProgressionCallback? = null,
+        ): FileMessage = toExternalResource().use {
+            contact.filesRoot.resolve(path).upload(it, callback)
+        }
+
+        /**
+         * 上传文件并获取文件消息.
+         *
+         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
+         *
+         * 需要调用方手动[关闭资源][ExternalResource.close].
+         *
+         * ## 已弃用
+         * 查看 [RemoteFile.upload] 获取更多信息.
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @since 2.5
+         * @see RemoteFile.path
+         * @see RemoteFile.upload
+         */
+        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmName("uploadAsFile")
+        @JvmOverloads
+        @Deprecated(
+            "Use sendAsFileTo instead.",
+            ReplaceWith(
+                "this.sendAsFileTo(contact, path, callback)",
+                "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo"
+            ),
+            level = DeprecationLevel.HIDDEN
+        ) // deprecated since 2.7-M1
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public suspend fun ExternalResource.uploadAsFile(
+            contact: FileSupported,
+            path: String,
+            callback: RemoteFile.ProgressionCallback? = null,
+        ): FileMessage {
+            return contact.filesRoot.resolve(path).upload(this, callback)
+        }
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region sendAsFileTo
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 上传文件并发送文件消息.
+         *
+         * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @since 2.5
+         * @see RemoteFile.path
+         * @see RemoteFile.uploadAndSend
+         */
+        @Suppress("DEPRECATION_ERROR", "DEPRECATION")
+        @Deprecated(
+            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
+            ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
+        ) // deprecated since 2.8.0-RC
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmOverloads
+        @DeprecatedSinceMirai(warningSince = "2.8")
+        public suspend fun <C : FileSupported> File.sendTo(
+            contact: C,
+            path: String,
+            callback: RemoteFile.ProgressionCallback? = null,
+        ): MessageReceipt<C> = toExternalResource().use {
+            contact.filesRoot.resolve(path).upload(it, callback).sendTo(contact)
+        }
+
+        /**
+         * 上传文件并发送件消息.  如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型.
+         *
+         * 需要调用方手动[关闭资源][ExternalResource.close].
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @since 2.5
+         * @see RemoteFile.path
+         * @see RemoteFile.uploadAndSend
+         */
+        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+        @Deprecated(
+            "Deprecated. Please use AbsoluteFolder.uploadNewFile",
+            ReplaceWith("contact.files.uploadNewFile(path, this, callback)")
+        ) // deprecated since 2.8.0-RC
+        @JvmStatic
+        @JvmBlockingBridge
+        @JvmName("sendAsFile")
+        @JvmOverloads
+        @DeprecatedSinceMirai(warningSince = "2.8")
+        public suspend fun <C : FileSupported> ExternalResource.sendAsFileTo(
+            contact: C,
+            path: String,
+            callback: RemoteFile.ProgressionCallback? = null,
+        ): MessageReceipt<C> {
+            return contact.filesRoot.resolve(path).upload(this, callback).sendTo(contact)
+        }
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region uploadAsVoice
+        ///////////////////////////////////////////////////////////////////////////
+
+        @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+        @JvmBlockingBridge
+        @JvmStatic
+        @Deprecated(
+            "Use `contact.uploadAudio(resource)` instead",
+            level = DeprecationLevel.HIDDEN
+        ) // deprecated since 2.7
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice {
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+            if (contact is Group) return contact.uploadAudio(this).toVoice()
+            else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
+        }
+        // endregion
+    }
+}

+ 4 - 4
mirai-core-api/src/commonMain/kotlin/utils/FileLogger.kt → mirai-core-api/src/jvmBaseMain/kotlin/utils/FileLogger.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.utils

+ 0 - 0
mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt → mirai-core-api/src/jvmBaseMain/kotlin/utils/LoggerAdapters.kt


+ 302 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt

@@ -0,0 +1,302 @@
+/*
+ * 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:JvmMultifileClass
+@file:JvmName("Utils")
+
+package net.mamoe.mirai.utils
+
+import net.mamoe.mirai.utils.*
+import kotlin.reflect.KClass
+
+/**
+ * 日志记录器.
+ *
+ * ## Mirai 日志系统
+ *
+ * Mirai 内建简单的日志系统, 即 [MiraiLogger]. [MiraiLogger] 的实现有 [SimpleLogger], [PlatformLogger], [SilentLogger].
+ *
+ * [MiraiLogger] 仅能处理简单的日志任务, 通常推荐使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 等日志库.
+ *
+ * ## 使用第三方日志库接管 Mirai 日志系统
+ *
+ * 使用 [LoggerAdapters], 将第三方日志 `Logger` 转为 [MiraiLogger]. 然后通过 [MiraiLogger.setDefaultLoggerCreator] 全局覆盖日志.
+ *
+ * ## 实现或使用 [MiraiLogger]
+ *
+ * 不建议实现或使用 [MiraiLogger]. 请优先考虑使用上述第三方框架. [MiraiLogger] 仅应用于兼容旧版本代码.
+ *
+ * @see SimpleLogger 简易 logger, 它将所有的日志记录操作都转移给 lambda `(String?, Throwable?) -> Unit`
+ * @see PlatformLogger 各个平台下的默认日志记录实现.
+ * @see SilentLogger 忽略任何日志记录操作的 logger 实例.
+ * @see LoggerAdapters
+ *
+ * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数.
+ */
+public actual interface MiraiLogger {
+
+    /**
+     * 可以 service 实现的方式覆盖.
+     *
+     * @since 2.7
+     */
+    public actual interface Factory {
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象的 class
+         * @param identity 对象标记 (备注)
+         */
+        public actual fun create(requester: KClass<*>, identity: String?): MiraiLogger =
+            this.create(requester.java, identity)
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象的 class
+         * @param identity 对象标记 (备注)
+         */
+        public fun create(requester: Class<*>, identity: String? = null): MiraiLogger
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象
+         */
+        public actual fun create(requester: KClass<*>): MiraiLogger = create(requester, null)
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象
+         */
+        public fun create(requester: Class<*>): MiraiLogger = create(requester, null)
+
+        public actual companion object INSTANCE : Factory by loadService(Factory::class, { DefaultFactory() })
+    }
+
+    public actual companion object {
+        /**
+         * 顶层日志, 仅供 Mirai 内部使用.
+         */
+        @MiraiInternalApi
+        @MiraiExperimentalApi
+        @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public actual val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
+
+        /**
+         * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
+         */
+        @Suppress("DeprecatedCallableAddReplaceWith")
+        @Deprecated(
+            "Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory",
+            level = DeprecationLevel.ERROR
+        ) // deprecated since 2.7
+        @JvmStatic
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
+        public actual fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
+            DefaultFactoryOverrides.override { _, identity -> creator(identity) }
+        }
+
+        /**
+         * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
+         *
+         * @see setDefaultLoggerCreator
+         */
+        @Deprecated(
+            "Please use MiraiLogger.Factory.create", ReplaceWith(
+                "MiraiLogger.Factory.create(YourClass::class, identity)",
+                "net.mamoe.mirai.utils.MiraiLogger"
+            ), level = DeprecationLevel.HIDDEN
+        ) // deprecated since 2.7
+        @JvmStatic
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public actual fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity)
+    }
+
+    /**
+     * 日志的标记. 在 Mirai 中, identity 可为
+     * - "Bot"
+     * - "BotNetworkHandler"
+     * 等.
+     *
+     * 它只用于帮助调试或统计. 十分建议清晰定义 identity
+     */
+    public actual val identity: String?
+
+    /**
+     * 获取 [MiraiLogger] 是否已开启
+     *
+     * 除 [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启.
+     */
+    public actual val isEnabled: Boolean
+
+    /**
+     * 当 VERBOSE 级别的日志启用时返回 `true`.
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isVerboseEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 DEBUG 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isDebugEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 INFO 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isInfoEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 WARNING 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isWarningEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 ERROR 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isErrorEnabled: Boolean get() = isEnabled
+
+    /**
+     * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用.
+     * [follower] 的存在可以让一次日志被多个日志记录器记录.
+     *
+     * 一般不建议直接修改这个属性. 请通过 [plus] 来连接两个日志记录器.
+     * 如: `val logger = bot.logger + MyLogger()`
+     * 当调用 `logger.info()` 时, `bot.logger` 会首先记录, `MyLogger` 会随后记录.
+     *
+     * 当然, 多个 logger 也可以加在一起: `val logger = bot.logger + MynLogger() + MyLogger2()`
+     */
+    @Suppress("UNUSED_PARAMETER")
+    @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+    public actual var follower: MiraiLogger?
+        get() = null
+        set(value) {}
+
+    /**
+     * 记录一个 `verbose` 级别的日志.
+     * 无关紧要的, 经常大量输出的日志应使用它.
+     */
+    public actual fun verbose(message: String?)
+
+    public actual fun verbose(e: Throwable?): Unit = verbose(null, e)
+    public actual fun verbose(message: String?, e: Throwable?)
+
+    /**
+     * 记录一个 _调试_ 级别的日志.
+     */
+    public actual fun debug(message: String?)
+
+    public actual fun debug(e: Throwable?): Unit = debug(null, e)
+    public actual fun debug(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _信息_ 级别的日志.
+     */
+    public actual fun info(message: String?)
+
+    public actual fun info(e: Throwable?): Unit = info(null, e)
+    public actual fun info(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _警告_ 级别的日志.
+     */
+    public actual fun warning(message: String?)
+
+    public actual fun warning(e: Throwable?): Unit = warning(null, e)
+    public actual fun warning(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _错误_ 级别的日志.
+     */
+    public actual fun error(message: String?)
+
+    public actual fun error(e: Throwable?): Unit = error(null, e)
+    public actual fun error(message: String?, e: Throwable?)
+
+    /** 根据优先级调用对应函数 */
+    public actual fun call(priority: SimpleLogger.LogPriority, message: String?, e: Throwable?): Unit =
+        priority.correspondingFunction(this, message, e)
+
+    /**
+     * 添加一个 [follower], 返回 [follower]
+     * 它只会把 `this` 的属性 [MiraiLogger.follower] 修改为这个函数的参数 [follower], 然后返回这个参数.
+     * 若 [MiraiLogger.follower] 已经有值, 则会替换掉这个值.
+     * ```
+     *   +------+      +----------+      +----------+      +----------+
+     *   | base | <--  | follower | <--  | follower | <--  | follower |
+     *   +------+      +----------+      +----------+      +----------+
+     * ```
+     *
+     * @return [follower]
+     */
+    @Suppress("DeprecatedCallableAddReplaceWith")
+    @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+    public actual operator fun <T : MiraiLogger> plus(follower: T): T = follower
+}
+
+
+internal object DefaultFactoryOverrides {
+    var override: ((requester: Class<*>, identity: String?) -> MiraiLogger)? =
+        null // 支持 LoggerAdapters 以及兼容旧版本
+
+    @JvmStatic
+    fun override(lambda: (requester: Class<*>, identity: String?) -> MiraiLogger) {
+        override = lambda
+    }
+
+    @JvmStatic
+    fun clearOverride() {
+        override = null
+    }
+}
+
+internal class DefaultFactory : MiraiLogger.Factory {
+    override fun create(requester: Class<*>, identity: String?): MiraiLogger {
+        val override = DefaultFactoryOverrides.override
+        return if (override != null) override(requester, identity) else PlatformLogger(
+            identity ?: requester.kotlin.simpleName ?: requester.simpleName
+        )
+    }
+}

+ 667 - 0
mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt

@@ -0,0 +1,667 @@
+/*
+ * 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("unused", "DEPRECATION")
+@file:JvmBlockingBridge
+
+package net.mamoe.mirai.utils
+
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.toList
+import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.FileSupported
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.FileMessage
+import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
+import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
+import java.io.File
+
+/**
+ * 表示一个远程文件或目录.
+ *
+ * [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的.
+ * 意味着操作的结果会因文件或目录在服务器中的状态变化而变化.
+ *
+ * 与 [File] 类似, [RemoteFile] 是不可变的. [renameTo] 和 [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性.
+ *
+ * ## 文件操作
+ *
+ * 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件.
+ *
+ * 示例:
+ * ```
+ * val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例
+ * val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例
+ *
+ *
+ * val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载
+ *
+ *
+ * val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息.
+ * group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送.
+ *
+ * file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本.
+ *
+ *
+ * // 要直接上传文件, 也可以简单地使用任一:
+ * group.uploadFile("/foo.txt", resource) // Kotlin
+ * resource.uploadAsFileTo(group, "/foo.txt") // Kotlin
+ * FileSupported.uploadFile(group, "/foo.txt", resource"); // Java
+ * ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java
+ * ```
+ *
+ * ## 目录操作
+ * [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录.
+ * ```
+ * val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例
+ *
+ * if (dir.exists()) { // 判断目录是否存在
+ *   // ...
+ * }
+ *
+ * dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表.
+ * dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表.
+ * ```
+ *
+ * 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" 和 "/xxx/foo.txt", 而 "/xxx/xxx/foo.txt" 不受支持.
+ *
+ * ## 文件名和目录名可重复
+ *
+ * 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠.
+ *
+ * 这个特性带来的行为有:
+ * - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件.
+ * - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器.
+ *
+ * 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id].
+ *
+ * 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` 的 [id].
+ * 服务器可以通过 [id] 准确定位重名文件中的某一个.
+ * 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件.
+ *
+ * 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件.
+ *
+ * @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口.
+ * @see FileSupported
+ * @since 2.5
+ */
+@Deprecated(
+    "Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
+    level = DeprecationLevel.WARNING
+) // deprecated since 2.8.0-RC
+@DeprecatedSinceMirai(warningSince = "2.8")
+@NotStableForInheritance
+public actual interface RemoteFile {
+    /**
+     * 文件名或目录名.
+     */
+    public actual val name: String
+
+    /**
+     * 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
+     */
+    public actual val id: String?
+
+    /**
+     * 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`.
+     *
+     * 根目录路径为 [ROOT_PATH]
+     */
+    public actual val path: String
+
+    /**
+     * 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null`
+     */
+    public actual val parent: RemoteFile?
+
+    /**
+     * 此文件所属的群或好友
+     */
+    public actual val contact: FileSupported
+
+    /**
+     * 当 [RemoteFile] 表示一个文件时返回 `true`.
+     */
+    public actual suspend fun isFile(): Boolean
+
+    /**
+     * 当 [RemoteFile] 表示一个目录时返回 `true`.
+     */
+    public actual suspend fun isDirectory(): Boolean = !isFile()
+
+    /**
+     * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定.
+     */
+    public actual suspend fun length(): Long
+
+    public actual class FileInfo @MiraiInternalApi actual constructor(
+        /**
+         * 文件或目录名.
+         */
+        public actual val name: String,
+        /**
+         * 唯一识别标识.
+         */
+        public actual val id: String,
+        /**
+         * 标准绝对路径.
+         */
+        public actual val path: String,
+        /**
+         * 文件长度 (大小) bytes, 目录的 [length] 为 0.
+         */
+        public actual val length: Long,
+        /**
+         * 下载次数. 目录没有下载次数, 此属性总是 `0`.
+         */
+        public actual val downloadTimes: Int,
+        /**
+         * 上传者 ID. 目录没有上传者, 此属性总是 `0`.
+         */
+        public actual val uploaderId: Long,
+        /**
+         * 上传的时间. 目录没有上传时间, 此属性总是 `0`.
+         */
+        public actual val uploadTime: Long,
+        /**
+         * 上次修改时间. 时间戳秒.
+         */
+        public actual val lastModifyTime: Long,
+        public actual val sha1: ByteArray,
+        public actual val md5: ByteArray,
+    ) {
+        /**
+         * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile].
+         */
+        public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
+            contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
+    }
+
+    /**
+     * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
+     */
+    public actual suspend fun getInfo(): FileInfo?
+
+    /**
+     * 当文件或目录存在时返回 `true`.
+     */
+    public actual suspend fun exists(): Boolean
+
+    /**
+     * @return [path]
+     */
+    public actual override fun toString(): String
+
+    ///////////////////////////////////////////////////////////////////////////
+    // resolve
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录.
+     *
+     * @param relative  相对路径. 当初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolve stdlib 内的类似函数
+     */
+    public actual fun resolve(relative: String): RemoteFile
+
+    /**
+     * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
+     *
+     * @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolve stdlib 内的类似函数
+     */
+    public actual fun resolve(relative: RemoteFile): RemoteFile
+
+    /**
+     * 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
+     * @see resolve
+     */
+    public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
+
+    /**
+     * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null`
+     * @see resolve
+     */
+    public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
+
+    /**
+     * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
+     * 不会检查 [RemoteFile] 是否表示一个目录.
+     *
+     * @param relative 当初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolveSibling stdlib 内的类似函数
+     */
+    public actual fun resolveSibling(relative: String): RemoteFile
+
+    /**
+     * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
+     * 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
+     *
+     * @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolveSibling stdlib 内的类似函数
+     */
+    public actual fun resolveSibling(relative: RemoteFile): RemoteFile
+
+    ///////////////////////////////////////////////////////////////////////////
+    // operations
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     */
+    public actual suspend fun delete(): Boolean
+
+    /**
+     * 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
+     * 操作非 Bot 自己上传的文件时需要管理员权限.
+     *
+     * [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     */
+    public actual suspend fun renameTo(name: String): Boolean
+
+    /**
+     * 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     *
+     * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     *
+     * **注意**: 与 [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如:
+     * ```
+     * val root = group.filesRoot
+     * root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt"
+     * root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同.
+
+     * root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”,相当于重命名文件
+     * ```
+     *
+     * @param target 目标文件位置.
+     */
+    public actual suspend fun moveTo(target: RemoteFile): Boolean
+
+    /**
+     * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     *
+     * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     *
+     * **已弃用:** 当 [path] 是绝对路径时, 这个函数运行正常;
+     * 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败.
+     *
+     * 使用参数为 [RemoteFile] 的 [moveTo] 代替.
+     *
+     * @suppress 在 2.6 弃用. 请使用 [moveTo]
+     */
+    @Deprecated(
+        "Use moveTo(RemoteFile) instead.",
+        replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"),
+        level = DeprecationLevel.ERROR
+    ) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
+    public actual suspend fun moveTo(path: String): Boolean {
+        // Impl notes:
+        // if `path` is absolute, this works as intended.
+        // if not, `resolve(path)` will be a child path from this dir and fails always.
+        return moveTo(resolve(path))
+    }
+
+    /**
+     * 创建目录. 目录已经存在或无管理员权限时返回 `false`.
+     *
+     * 创建后 [isDirectory] 也不一定会返回 `true`.
+     * 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器.
+     */
+    public actual suspend fun mkdir(): Boolean
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow].
+     *
+     * 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询.
+     */
+    public actual suspend fun listFiles(): Flow<RemoteFile>
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器.
+     * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表.
+     */
+    @JavaFriendlyAPI
+    public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList].
+     */
+    public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
+
+    /**
+     * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
+     */
+    public actual suspend fun toMessage(): FileMessage?
+
+    ///////////////////////////////////////////////////////////////////////////
+    // upload & download
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 上传进度回调, 可供前端使用, 以提供进度显示.
+     * @see asProgressionCallback
+     */
+    @Deprecated(
+        "Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile",
+        ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
+        level = DeprecationLevel.WARNING
+    ) // deprecated since 2.8.0-RC
+    @DeprecatedSinceMirai(warningSince = "2.8")
+    public actual interface ProgressionCallback {
+        /**
+         * 当上传开始时调用
+         */
+        public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
+
+        /**
+         * 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
+         *
+         * 提示: 可通过 [ExternalResource.size] 获取文件总大小.
+         */
+        public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
+
+        /**
+         * 当上传成功时调用
+         */
+        public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
+
+        /**
+         * 当上传以异常失败时调用
+         */
+        public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
+
+        public actual companion object {
+            /**
+             * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
+             *
+             * 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer]到 [SendChannel] 中.
+             * 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存.
+             *
+             * 若 [closeOnFinish] 为 `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel].
+             *
+             * 使用示例:
+             * ```
+             * val progress = Channel<Long>(Channel.BUFFERED)
+             *
+             * launch {
+             *   // 每 3 秒发送一次上传进度百分比
+             *   progress.receiveAsFlow().sample(3.seconds).collect { bytes ->
+             *     group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数
+             *   }
+             * }
+             *
+             * group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true))
+             * group.sendMessage("File uploaded successfully.")
+             * ```
+             *
+             * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
+             */
+            @JvmStatic
+            public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
+                return object : ProgressionCallback {
+                    override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
+                        trySend(downloadedSize)
+                    }
+
+                    override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
+                        if (closeOnFinish) this@asProgressionCallback.close()
+                    }
+
+                    override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
+                        if (closeOnFinish) this@asProgressionCallback.close(exception)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度.
+     *
+     * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件.
+     * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend].
+     *
+     * ## 已弃用
+     *
+     * 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息.
+     * 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作.
+     * 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替.
+     *
+     * 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250)
+     *
+     *
+     * **注意**: [resource] 仅表示资源数据, 而不带有文件名属性.
+     * 与 [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例:
+     * ```
+     * group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录.
+     * group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录.
+     *
+     * val root = group.filesRoot
+     * root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt".
+     * root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt".
+     * ```
+     *
+     * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+     * @param callback 进度回调
+     * @throws IllegalStateException 该文件上传失败或权限不足时抛出
+     */
+    @Deprecated(
+        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR
+    ) // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public actual suspend fun upload(
+        resource: ExternalResource,
+        callback: ProgressionCallback?,
+    ): FileMessage
+
+    /**
+     * 上传文件到 [RemoteFile.path] 表示的路径.
+     * ## 已弃用
+     * 阅读 [upload] 获取更多信息
+     * @see upload
+     */
+    @Suppress("DEPRECATION_ERROR")
+    @Deprecated(
+        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
+    )  // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
+
+    /**
+     * 上传文件.
+     * ## 已弃用
+     * 阅读 [upload] 获取更多信息
+     * @see upload
+     */
+    @Suppress("DEPRECATION_ERROR")
+    @Deprecated(
+        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(file, callback)"), DeprecationLevel.ERROR
+    ) // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public suspend fun upload(
+        file: File,
+        callback: ProgressionCallback? = null,
+    ): FileMessage = file.toExternalResource().use { upload(it, callback) }
+
+    /**
+     * 上传文件.
+     * ## 已弃用
+     * 阅读 [upload] 获取更多信息
+     * @see upload
+     */
+    @Suppress("DEPRECATION_ERROR")
+    @Deprecated(
+        "Use sendFile instead.", ReplaceWith("this.uploadAndSend(file)"), DeprecationLevel.ERROR
+    ) // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public suspend fun upload(file: File): FileMessage = file.toExternalResource().use { upload(it) }
+
+    /**
+     * 上传文件并发送文件消息.
+     *
+     * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件.
+     * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件,
+     * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件.
+     *
+     * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+     * @see upload
+     */
+    @MiraiExperimentalApi
+    public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
+
+    /**
+     * 上传文件并发送文件消息.
+     * @see uploadAndSend
+     */
+    @MiraiExperimentalApi
+    public suspend fun uploadAndSend(file: File): MessageReceipt<Contact> =
+        file.toExternalResource().use { uploadAndSend(it) }
+
+    /**
+     * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
+     */
+    public actual suspend fun getDownloadInfo(): DownloadInfo?
+
+    public actual class DownloadInfo @MiraiInternalApi actual constructor(
+        /**
+         * @see RemoteFile.name
+         */
+        public actual val filename: String,
+        /**
+         * @see RemoteFile.id
+         */
+        public actual val id: String,
+        /**
+         * 标准绝对路径
+         * @see RemoteFile.path
+         */
+        public actual val path: String,
+        /**
+         * HTTP or HTTPS URL
+         */
+        public actual val url: String,
+        public actual val sha1: ByteArray,
+        public actual val md5: ByteArray,
+    ) {
+        actual override fun toString(): String {
+            return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
+                    "md5=${md5.toUHexString("")})"
+        }
+    }
+
+    public actual companion object {
+        /**
+         * 根目录路径
+         * @see RemoteFile.path
+         */
+        public actual const val ROOT_PATH: String = "/"
+
+        /**
+         * 上传文件并获取文件消息, 但不发送.
+         *
+         * ## 已弃用
+         * 在 [upload] 获取更多信息
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+         * @see RemoteFile.upload
+         */
+        @JvmStatic
+        @JvmOverloads
+        @Deprecated(
+            "Use sendFile instead.",
+            ReplaceWith(
+                "this.sendFile(path, resource, callback)",
+                "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
+            ),
+            level = DeprecationLevel.ERROR
+        ) // deprecated since 2.7-M1
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+        public actual suspend fun FileSupported.uploadFile(
+            path: String,
+            resource: ExternalResource,
+            callback: ProgressionCallback?,
+        ): FileMessage =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
+
+        /**
+         * 上传文件并获取文件消息, 但不发送.
+         * ## 已弃用
+         * 阅读 [uploadFile] 获取更多信息.
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @see RemoteFile.upload
+         */
+        @JvmStatic
+        @JvmOverloads
+        @Deprecated(
+            "Use sendFile instead.",
+            ReplaceWith(
+                "this.sendFile(path, file, callback)",
+                "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
+            ),
+            level = DeprecationLevel.ERROR
+        ) // deprecated since 2.7-M1
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+        public suspend fun FileSupported.uploadFile(
+            path: String,
+            file: File,
+            callback: ProgressionCallback? = null,
+        ): FileMessage =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(file, callback)
+
+        /**
+         * 上传文件并发送文件消息到相关 [FileSupported].
+         * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+         * @see RemoteFile.uploadAndSend
+         */
+        @JvmStatic
+        @JvmOverloads
+        @Deprecated(
+            "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
+            ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
+            level = DeprecationLevel.WARNING
+        ) // deprecated since 2.8.0-RC
+        @DeprecatedSinceMirai(warningSince = "2.8")
+        public actual suspend fun <C : FileSupported> C.sendFile(
+            path: String,
+            resource: ExternalResource,
+            callback: ProgressionCallback?,
+        ): MessageReceipt<C> =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+            this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
+
+        /**
+         * 上传文件并发送文件消息到相关 [FileSupported].
+         * @see RemoteFile.uploadAndSend
+         */
+        @JvmStatic
+        @JvmOverloads
+        @Deprecated(
+            "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
+            ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
+            level = DeprecationLevel.WARNING
+        ) // deprecated since 2.8.0-RC
+        @DeprecatedSinceMirai(warningSince = "2.8")
+        public suspend fun <C : FileSupported> C.sendFile(
+            path: String,
+            file: File,
+            callback: ProgressionCallback? = null,
+        ): MessageReceipt<C> =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+            this.filesRoot.resolve(path).upload(file, callback).sendTo(this)
+    }
+}

+ 4 - 4
mirai-core-api/src/commonMain/kotlin/utils/SingleFileLogger.kt → mirai-core-api/src/jvmBaseMain/kotlin/utils/SingleFileLogger.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmName("FileLoggerKt") // bin-comp

+ 4 - 4
mirai-core-api/src/commonMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt → mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.internal.utils

+ 10 - 0
mirai-core-api/src/jvmMain/kotlin/package.kt

@@ -0,0 +1,10 @@
+/*
+ * 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

+ 95 - 0
mirai-core-api/src/nativeMain/kotlin/contact/announcement/Announcements.kt

@@ -0,0 +1,95 @@
+/*
+ * 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.contact.announcement
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.toList
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.contact.PermissionDeniedException
+import net.mamoe.mirai.utils.ExternalResource
+import net.mamoe.mirai.utils.NotStableForInheritance
+
+/**
+ * 表示一个群的公告列表 (管理器).
+ *
+ * ## 获取群公告
+ *
+ * ### 获取 [Announcements] 实例
+ *
+ * 只可以通过 [Group.announcements] 获取一个群的公告列表, 即 [Announcements] 实例.
+ *
+ * ### 获取公告列表
+ *
+ * 通过 [asFlow] 可以获取到*惰性*流, 在从流中收集数据时才会请求服务器获取数据.
+ *
+ * 若要获取全部公告列表, 可使用 [toList].
+ *
+ * ## 发布群公告
+ *
+ * 查看 [Announcement]
+ *
+ * @since 2.7
+ */
+@NotStableForInheritance
+public actual interface Announcements {
+    /**
+     * 创建一个能获取该群内所有群公告列表的 [Flow]. 在 [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
+     *
+     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] 和 [Flow] 的[收集][Flow.collect].
+     */
+    public actual suspend fun asFlow(): Flow<OnlineAnnouncement>
+
+    /**
+     * 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
+     *
+     * 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取并返回已经成功获取到的 [OfflineAnnouncement] 列表.
+     *
+     * @return 此时刻的群公告只读列表.
+     */
+    public actual suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList()
+
+
+    /**
+     * 删除一条群公告. 需要管理员权限. 使用 [OnlineAnnouncement.delete] 与此方法效果相同.
+     *
+     * @param fid 公告的 [OnlineAnnouncement.fid]
+     * @return 成功返回 `true`, 群公告不存在时返回 `false`
+     *
+     * @throws PermissionDeniedException 当没有权限时抛出
+     * @throws IllegalStateException 当协议异常时抛出
+     *
+     * @see OnlineAnnouncement.delete
+     */
+    public actual suspend fun delete(fid: String): Boolean
+
+    /**
+     * 获取一条群公告.
+     * @param fid 公告的 [OnlineAnnouncement.fid]
+     * @return 返回 `null` 表示不存在该 [fid] 的群公告
+     * @throws IllegalStateException 当协议异常时抛出
+     */
+    public actual suspend fun get(fid: String): OnlineAnnouncement?
+
+    /**
+     * 在该群发布群公告并获得 [OnlineAnnouncement], 需要管理员权限. 发布公告后群内将会出现 "有新公告" 系统提示.
+     * @throws PermissionDeniedException 当没有权限时抛出
+     * @throws IllegalStateException 当协议异常时抛出
+     * @see Announcement.publishTo
+     */
+    public actual suspend fun publish(announcement: Announcement): OnlineAnnouncement
+
+    /**
+     * 上传资源作为群公告图片. 返回值可用于 [AnnouncementParameters.image].
+     *
+     * **注意**: 需要由调用方[关闭][ExternalResource.close] [resource].
+     * @throws IllegalStateException 当协议异常时抛出
+     */
+    public actual suspend fun uploadImage(resource: ExternalResource): AnnouncementImage
+}

+ 10 - 0
mirai-core-api/src/nativeMain/kotlin/package.kt

@@ -0,0 +1,10 @@
+/*
+ * 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

+ 35 - 0
mirai-core-api/src/nativeMain/kotlin/spi/SPIServiceLoader.kt

@@ -0,0 +1,35 @@
+/*
+ * 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.spi
+
+import net.mamoe.mirai.utils.MiraiLogger
+import kotlin.reflect.KClass
+
+internal actual class SPIServiceLoader<T : BaseService> actual constructor(
+    defaultService: T,
+    private val serviceType: KClass<T>
+) {
+    actual var service: T = defaultService
+
+    actual fun reload() {
+        TODO("native")
+    }
+
+    init {
+        reload()
+    }
+
+    actual companion object {
+        actual val SPI_SERVICE_LOADER_LOGGER: MiraiLogger by lazy {
+            MiraiLogger.Factory.create(SPIServiceLoader::class, "spi-service-loader")
+        }
+    }
+
+}

+ 554 - 0
mirai-core-api/src/nativeMain/kotlin/utils/BotConfiguration.kt

@@ -0,0 +1,554 @@
+/*
+ * 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.utils
+
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.io.core.Input
+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()
+ *     ...
+ * }})
+ * ```
+ */
+@Suppress("PropertyName")
+public actual open class BotConfiguration { // open for Java
+    /**
+     * 工作目录. 默认为当前目录
+     */
+    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 = {
+            Companion.json.decodeFromString(DeviceInfo.serializer(), json)
+        }
+    }
+
+    /**
+     * 使用文件存储设备信息.
+     *
+     * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常.
+     * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
+     * @see deviceInfo
+     */
+    @ConfigurationDsl
+    public actual fun fileBasedDeviceInfo(filepath: String) {
+        deviceInfo = TODO("native")
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // 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
+    //////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 缓存数据目录, 相对于 [workingDir].
+     *
+     * 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
+     * 默认启用的缓存可以加快登录过程.
+     *
+     * 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
+     * - 联系人列表
+     * - 登录服务器列表
+     * - 资源服务秘钥
+     *
+     * 其他内容如通过 [Input] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
+     *
+     * @since 2.4
+     */
+    public var cacheDir: String = workingDir + "/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
+        }
+    }
+}

+ 121 - 0
mirai-core-api/src/nativeMain/kotlin/utils/DeviceInfo.kt

@@ -0,0 +1,121 @@
+/*
+ * 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.utils
+
+import kotlinx.io.core.toByteArray
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlin.random.Random
+
+@Serializable
+public actual class DeviceInfo actual constructor(
+    public actual val display: ByteArray,
+    public actual val product: ByteArray,
+    public actual val device: ByteArray,
+    public actual val board: ByteArray,
+    public actual val brand: ByteArray,
+    public actual val model: ByteArray,
+    public actual val bootloader: ByteArray,
+    public actual val fingerprint: ByteArray,
+    public actual val bootId: ByteArray,
+    public actual val procVersion: ByteArray,
+    public actual val baseBand: ByteArray,
+    public actual val version: Version,
+    public actual val simInfo: ByteArray,
+    public actual val osType: ByteArray,
+    public actual val macAddress: ByteArray,
+    public actual val wifiBSSID: ByteArray,
+    public actual val wifiSSID: ByteArray,
+    public actual val imsiMd5: ByteArray,
+    public actual val imei: String,
+    public actual val apn: ByteArray
+) {
+    public actual val androidId: ByteArray get() = display
+    public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
+
+    init {
+        require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." }
+    }
+
+    @Transient
+    @MiraiInternalApi
+    public actual val guid: ByteArray = generateGuid(androidId, macAddress)
+
+    @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // serializable
+    @Serializable
+    public actual class Version actual constructor(
+        public actual val incremental: ByteArray = "5891938".toByteArray(),
+        public actual val release: ByteArray = "10".toByteArray(),
+        public actual val codename: ByteArray = "REL".toByteArray(),
+        public actual val sdk: Int = 29
+    ) {
+        /**
+         * @since 2.9
+         */
+        actual override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Version) return false
+
+            if (!incremental.contentEquals(other.incremental)) return false
+            if (!release.contentEquals(other.release)) return false
+            if (!codename.contentEquals(other.codename)) return false
+            if (sdk != other.sdk) return false
+
+            return true
+        }
+
+        /**
+         * @since 2.9
+         */
+        actual override fun hashCode(): Int {
+            var result = incremental.contentHashCode()
+            result = 31 * result + release.contentHashCode()
+            result = 31 * result + codename.contentHashCode()
+            result = 31 * result + sdk
+            return result
+        }
+    }
+
+    public actual companion object {
+        internal actual val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
+
+        /**
+         * 生成随机 [DeviceInfo]
+         *
+         * @since 2.0
+         */
+        public actual fun random(): DeviceInfo = random(Random.Default)
+
+        /**
+         * 使用特定随机数生成器生成 [DeviceInfo]
+         *
+         * @since 2.9
+         */
+        public actual fun random(random: Random): DeviceInfo {
+            return DeviceInfoCommonImpl.randomDeviceInfo(random)
+        }
+    }
+
+    /**
+     * @since 2.9
+     */
+    @Suppress("DuplicatedCode")
+    actual override fun equals(other: Any?): Boolean {
+        return DeviceInfoCommonImpl.equalsImpl(this, other)
+    }
+
+
+    /**
+     * @since 2.9
+     */
+    actual override fun hashCode(): Int {
+        return DeviceInfoCommonImpl.hashCodeImpl(this)
+    }
+}

+ 273 - 0
mirai-core-api/src/nativeMain/kotlin/utils/ExternalResource.kt

@@ -0,0 +1,273 @@
+/*
+ * 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.utils
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.io.core.Input
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.Contact.Companion.sendImage
+import net.mamoe.mirai.contact.Contact.Companion.uploadImage
+import net.mamoe.mirai.internal.utils.*
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.Image
+import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
+import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
+import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
+
+/**
+ * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
+ *
+ * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
+ *
+ * # 创建
+ * - [ByteArray.toExternalResource]
+ *
+ * ## 在 Kotlin 获得和使用 [ExternalResource] 实例
+ *
+ * ```
+ * file.toExternalResource().use { resource -> // 安全地使用资源
+ *     contact.uploadImage(resource) // 用来上传图片
+ *     contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
+ * }
+ * ```
+ *
+ * 注意, 若使用 [Input], 必须手动关闭 [Input]. 一种使用情况示例:
+ *
+ * ```
+ * inputStream.use { input -> // 安全地使用 InputStream
+ *     input.toExternalResource().use { resource -> // 安全地使用资源
+ *         contact.uploadImage(resource) // 用来上传图片
+ *         contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
+ *     }
+ * }
+ * ```
+ *
+ * ## 在 Java 获得和使用 [ExternalResource] 实例
+ *
+ * ```
+ * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
+ *     contact.uploadImage(resource); // 用来上传图片
+ *     contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+ * }
+ * ```
+ *
+ * 注意, 若使用 [Input], 必须手动关闭 [Input]. 一种使用情况示例:
+ *
+ * ```java
+ * try (InputStream stream = ...) { // 安全地使用 InputStream
+ *     try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
+ *         contact.uploadImage(resource); // 用来上传图片
+ *         contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
+ *     }
+ * }
+ * ```
+ *
+ * # 释放
+ *
+ * 当 [ExternalResource] 创建时就可能会打开一些资源.
+ * 类似于 [Input], [ExternalResource] 需要被 [关闭][close].
+ *
+ * ## 未释放资源的补救策略
+ *
+ * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放.
+ * 这依赖于 JVM 垃圾收集策略, 因此不可靠, 资源仍然需要手动 close.
+ *
+ * ## 使用单次自动释放
+ *
+ * 若创建的资源仅需要*很快地*使用一次, 可使用 [toAutoCloseable] 获得在使用一次后就会自动关闭的资源.
+ *
+ * 示例:
+ * ```java
+ * contact.uploadImage(ExternalResource.create(file).toAutoCloseable()); // 创建并立即使用单次自动释放的资源
+ * ```
+ *
+ * **注意**: 如果仅使用 [toAutoCloseable] 而不通过 [Contact.uploadImage] 等 mirai 内置方法使用资源, 资源仍然会处于打开状态且不会被自动关闭.
+ * 最终资源会由上述*未释放资源的补救策略*关闭, 但这依赖于 JVM 垃圾收集策略而不可靠.
+ * 因此建议在创建单次自动释放的资源后就尽快使用它, 否则仍然需要考虑在正确的时间及时关闭资源.
+ *
+ * # 实现 [ExternalResource]
+ *
+ * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用.
+ *
+ * 建议继承 [AbstractExternalResource], 这将支持上文提到的资源自动释放功能.
+ *
+ * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值.
+ *
+ * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image]
+ * @see ExternalResource.sendAsImageTo 将资源作为图片发送
+ * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image]
+ * @see Contact.sendImage 发送一个资源作为图片
+ *
+ * @see FileCacheStrategy
+ */
+public actual interface ExternalResource : Closeable {
+
+    /**
+     * 是否在 _使用一次_ 后自动 [close].
+     *
+     * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功.
+     *
+     * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为.
+     *
+     * @since 2.8
+     */
+    @MiraiExperimentalApi
+    public actual val isAutoClose: Boolean
+        get() = false
+
+    /**
+     * 文件内容 MD5. 16 bytes
+     */
+    public actual val md5: ByteArray
+
+    /**
+     * 文件内容 SHA1. 16 bytes
+     * @since 2.5
+     */
+    public actual val sha1: ByteArray
+        get() =
+            throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
+    // 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
+    // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.
+
+
+    /**
+     * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME].
+     *
+     * 默认会从文件头识别, 支持的文件类型:
+     * png, jpg, gif, tif, bmp, amr, silk
+     *
+     * @see net.mamoe.mirai.utils.getFileType
+     * @see net.mamoe.mirai.utils.FILE_TYPES
+     * @see DEFAULT_FORMAT_NAME
+     */
+    public actual val formatName: String
+
+    /**
+     * 文件大小 bytes
+     */
+    public actual val size: Long
+
+    /**
+     * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred].
+     */
+    public actual val closed: Deferred<Unit>
+
+    /**
+     * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流.
+     *
+     * 关闭此流不会关闭 [ExternalResource].
+     * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
+     *
+     * @since SINCE_NATIVE_TARGET
+     */
+    public actual fun input(): Input
+
+    @MiraiInternalApi
+    public actual fun calculateResourceId(): String {
+        return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
+    }
+
+    /**
+     * 该 [ExternalResource] 的数据来源, 可能有以下的返回
+     *
+     * - [ByteArray] RAM
+     * - ...
+     *
+     * implementation note:
+     *
+     * - 对于无法二次读取的数据来源 (如 [Input]), 返回 `null`
+     * - 不要返回 [String], 没有约定 [String] 代表什么
+     * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如文件句柄)
+     *
+     * @since 2.8.0
+     */
+    public actual val origin: Any? get() = null
+
+    /**
+     * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource].
+     *
+     * @since 2.8.0
+     */
+    public actual fun toAutoCloseable(): ExternalResource {
+        return if (isAutoClose) this else {
+            val delegate = this
+            object : ExternalResource by delegate {
+                override val isAutoClose: Boolean get() = true
+                override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)"
+                override fun toAutoCloseable(): ExternalResource {
+                    return this
+                }
+            }
+        }
+    }
+
+
+    public actual companion object {
+        /**
+         * 在无法识别文件格式时使用的默认格式名. "mirai".
+         *
+         * @see ExternalResource.formatName
+         */
+        public actual const val DEFAULT_FORMAT_NAME: String = "mirai"
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region toExternalResource
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
+         *
+         * @param formatName 查看 [ExternalResource.formatName]
+         */
+        public actual fun ByteArray.toExternalResource(formatName: String?): ExternalResource =
+            ExternalResourceImplByByteArray(this, formatName)
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region sendAsImageTo
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 将图片作为单独的消息发送给指定联系人.
+         *
+         * **注意**:本函数不会关闭 [ExternalResource].
+         *
+         * @see Contact.uploadImage 上传图片
+         * @see Contact.sendMessage 最终调用, 发送消息.
+         *
+         * @throws OverFileSizeMaxException
+         */
+        public actual suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
+            contact.uploadImage(this).sendTo(contact)
+
+        // endregion
+
+        ///////////////////////////////////////////////////////////////////////////
+        // region uploadAsImage
+        ///////////////////////////////////////////////////////////////////////////
+
+        /**
+         * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
+         *
+         * **注意**:本函数不会关闭 [ExternalResource].
+         *
+         * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
+         *
+         * @see Contact.uploadImage 最终调用, 上传图片.
+         */
+        public actual suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)
+
+        // endregion
+    }
+}

+ 85 - 0
mirai-core-api/src/nativeMain/kotlin/utils/LoginSolver.kt

@@ -0,0 +1,85 @@
+/*
+ * 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.utils
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.network.LoginFailedException
+import net.mamoe.mirai.utils.LoginSolver.Companion.Default
+
+/**
+ * 验证码, 设备锁解决器
+ *
+ * @see Default
+ * @see BotConfiguration.loginSolver
+ */
+public actual abstract class LoginSolver actual constructor() {
+    /**
+     * 处理图片验证码.
+     *
+     * 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
+     *
+     * @throws LoginFailedException
+     */
+    public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
+
+    /**
+     * 为 `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
+     * 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
+     */
+    public actual open val isSliderCaptchaSupported: Boolean
+        get() = false
+
+    /**
+     * 处理滑动验证码.
+     *
+     * 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
+     *
+     * @throws LoginFailedException
+     * @return 验证码解决成功后获得的 ticket.
+     */
+    public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
+
+    /**
+     * 处理不安全设备验证.
+     *
+     * 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
+     * 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
+     *
+     * @return 任意内容. 返回值保留以供未来更新.
+     * @throws LoginFailedException
+     */
+    public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(
+        bot: Bot,
+        url: String
+    ): String?
+
+    public actual companion object {
+        /**
+         * 当前平台默认的 [LoginSolver]。
+         *
+         * 检测策略:
+         * 1. 若是 `mirai-core-api-android` 或 `android.util.Log` 存在, 返回 `null`.
+         * 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 `StandardCharImageLoginSolver`
+         * 3. 检测 JVM 桌面环境, 若支持, 返回 `SwingSolver`
+         * 4. 返回 `StandardCharImageLoginSolver`
+         *
+         * @return `SwingSolver` 或 `StandardCharImageLoginSolver` 或 `null`
+         */
+        public actual val Default: LoginSolver?
+            get() = TODO("Not yet implemented")
+
+        @Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
+        @Suppress("unused")
+        public actual fun getDefault(): LoginSolver = Default
+            ?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
+    }
+}

+ 254 - 0
mirai-core-api/src/nativeMain/kotlin/utils/MiraiLogger.kt

@@ -0,0 +1,254 @@
+/*
+ * 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.utils
+
+import kotlin.reflect.KClass
+
+/**
+ * 日志记录器.
+ *
+ * ## Mirai 日志系统
+ *
+ * Mirai 内建简单的日志系统, 即 [MiraiLogger]. [MiraiLogger] 的实现有 [SimpleLogger], [PlatformLogger], [SilentLogger].
+ *
+ * [MiraiLogger] 仅能处理简单的日志任务, 通常推荐使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 等日志库.
+ *
+ * ## 使用第三方日志库接管 Mirai 日志系统
+ *
+ * 使用 [LoggerAdapters], 将第三方日志 `Logger` 转为 [MiraiLogger]. 然后通过 [MiraiLogger.setDefaultLoggerCreator] 全局覆盖日志.
+ *
+ * ## 实现或使用 [MiraiLogger]
+ *
+ * 不建议实现或使用 [MiraiLogger]. 请优先考虑使用上述第三方框架. [MiraiLogger] 仅应用于兼容旧版本代码.
+ *
+ * @see SimpleLogger 简易 logger, 它将所有的日志记录操作都转移给 lambda `(String?, Throwable?) -> Unit`
+ * @see PlatformLogger 各个平台下的默认日志记录实现.
+ * @see SilentLogger 忽略任何日志记录操作的 logger 实例.
+ * @see LoggerAdapters
+ *
+ * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数.
+ */
+public actual interface MiraiLogger {
+    /**
+     * 可以 service 实现的方式覆盖.
+     *
+     * @since 2.7
+     */
+    public actual interface Factory {
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象的 class
+         * @param identity 对象标记 (备注)
+         */
+        public actual fun create(requester: KClass<*>, identity: String?): MiraiLogger = TODO("native")
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象
+         */
+        public actual fun create(requester: KClass<*>): MiraiLogger = TODO("native")
+
+        public actual companion object INSTANCE : Factory by TODO("native")
+    }
+
+    public actual companion object {
+        /**
+         * 顶层日志, 仅供 Mirai 内部使用.
+         */
+        @MiraiInternalApi
+        @MiraiExperimentalApi
+        @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public actual val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
+
+        /**
+         * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
+         */
+        @Suppress("DeprecatedCallableAddReplaceWith")
+        @Deprecated(
+            "Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory",
+            level = DeprecationLevel.ERROR
+        ) // deprecated since 2.7
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
+        public actual fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
+            throw UnsupportedOperationException()
+        }
+
+        /**
+         * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
+         *
+         * @see setDefaultLoggerCreator
+         */
+        @Deprecated(
+            "Please use MiraiLogger.Factory.create", ReplaceWith(
+                "MiraiLogger.Factory.create(YourClass::class, identity)",
+                "net.mamoe.mirai.utils.MiraiLogger"
+            ), level = DeprecationLevel.HIDDEN
+        ) // deprecated since 2.7
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+        public actual fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity)
+    }
+
+    /**
+     * 日志的标记. 在 Mirai 中, identity 可为
+     * - "Bot"
+     * - "BotNetworkHandler"
+     * 等.
+     *
+     * 它只用于帮助调试或统计. 十分建议清晰定义 identity
+     */
+    public actual val identity: String?
+
+    /**
+     * 获取 [MiraiLogger] 是否已开启
+     *
+     * 除 [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启.
+     */
+    public actual val isEnabled: Boolean
+
+    /**
+     * 当 VERBOSE 级别的日志启用时返回 `true`.
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isVerboseEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 DEBUG 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isDebugEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 INFO 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isInfoEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 WARNING 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isWarningEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 ERROR 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public actual val isErrorEnabled: Boolean get() = isEnabled
+
+    /**
+     * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用.
+     * [follower] 的存在可以让一次日志被多个日志记录器记录.
+     *
+     * 一般不建议直接修改这个属性. 请通过 [plus] 来连接两个日志记录器.
+     * 如: `val logger = bot.logger + MyLogger()`
+     * 当调用 `logger.info()` 时, `bot.logger` 会首先记录, `MyLogger` 会随后记录.
+     *
+     * 当然, 多个 logger 也可以加在一起: `val logger = bot.logger + MynLogger() + MyLogger2()`
+     */
+    @Suppress("UNUSED_PARAMETER")
+    @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+    public actual var follower: MiraiLogger?
+        get() = null
+        set(value) {}
+
+    /**
+     * 记录一个 `verbose` 级别的日志.
+     * 无关紧要的, 经常大量输出的日志应使用它.
+     */
+    public actual fun verbose(message: String?)
+
+    public actual fun verbose(e: Throwable?): Unit = verbose(null, e)
+    public actual fun verbose(message: String?, e: Throwable?)
+
+    /**
+     * 记录一个 _调试_ 级别的日志.
+     */
+    public actual fun debug(message: String?)
+
+    public actual fun debug(e: Throwable?): Unit = debug(null, e)
+    public actual fun debug(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _信息_ 级别的日志.
+     */
+    public actual fun info(message: String?)
+
+    public actual fun info(e: Throwable?): Unit = info(null, e)
+    public actual fun info(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _警告_ 级别的日志.
+     */
+    public actual fun warning(message: String?)
+
+    public actual fun warning(e: Throwable?): Unit = warning(null, e)
+    public actual fun warning(message: String?, e: Throwable?)
+
+
+    /**
+     * 记录一个 _错误_ 级别的日志.
+     */
+    public actual fun error(message: String?)
+
+    public actual fun error(e: Throwable?): Unit = error(null, e)
+    public actual fun error(message: String?, e: Throwable?)
+
+    /** 根据优先级调用对应函数 */
+    public actual fun call(priority: SimpleLogger.LogPriority, message: String?, e: Throwable?): Unit =
+        priority.correspondingFunction(this, message, e)
+
+    /**
+     * 添加一个 [follower], 返回 [follower]
+     * 它只会把 `this` 的属性 [MiraiLogger.follower] 修改为这个函数的参数 [follower], 然后返回这个参数.
+     * 若 [MiraiLogger.follower] 已经有值, 则会替换掉这个值.
+     * ```
+     *   +------+      +----------+      +----------+      +----------+
+     *   | base | <--  | follower | <--  | follower | <--  | follower |
+     *   +------+      +----------+      +----------+      +----------+
+     * ```
+     *
+     * @return [follower]
+     */
+    @Suppress("DeprecatedCallableAddReplaceWith")
+    @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
+    public actual operator fun <T : MiraiLogger> plus(follower: T): T = follower
+}

+ 60 - 0
mirai-core-api/src/nativeMain/kotlin/utils/PlatformLogger.kt

@@ -0,0 +1,60 @@
+/*
+ * 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.utils
+
+/**
+ * 当前平台的默认的日志记录器.
+ * - 在 _JVM 控制台_ 端的实现为 [println]
+ * - 在 _Android_ 端的实现为 `android.util.Log`
+ *
+ *
+ * 单条日志格式 (正则) 为:
+ * ```regex
+ * ^([\w-]*\s[\w:]*)\s(\w)\/(.*?):\s(.+)$
+ * ```
+ * 其中 group 分别为: 日期与时间, 严重程度, [identity], 消息内容.
+ *
+ * 示例:
+ * ```log
+ * 2020-05-21 19:51:09 V/Bot 123456789: Send: OidbSvc.0x88d_7
+ * ```
+ *
+ * 日期时间格式为 `yyyy-MM-dd HH:mm:ss`,
+ *
+ * 严重程度为 V, I, W, E. 分别对应 verbose, info, warning, error
+ *
+ * @see MiraiLogger.create
+ */
+@MiraiInternalApi
+public actual open class PlatformLogger actual constructor(identity: String?) :
+    MiraiLoggerPlatformBase() {
+    override val identity: String?
+        get() = TODO("Not yet implemented")
+
+    override fun verbose0(message: String?, e: Throwable?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun debug0(message: String?, e: Throwable?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun info0(message: String?, e: Throwable?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun warning0(message: String?, e: Throwable?) {
+        TODO("Not yet implemented")
+    }
+
+    override fun error0(message: String?, e: Throwable?) {
+        TODO("Not yet implemented")
+    }
+}

+ 575 - 0
mirai-core-api/src/nativeMain/kotlin/utils/RemoteFile.kt

@@ -0,0 +1,575 @@
+/*
+ * 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("unused", "DEPRECATION")
+
+package net.mamoe.mirai.utils
+
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.toList
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.contact.FileSupported
+import net.mamoe.mirai.contact.Group
+import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.FileMessage
+import net.mamoe.mirai.message.data.sendTo
+import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
+import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
+
+/**
+ * 表示一个远程文件或目录.
+ *
+ * [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的.
+ * 意味着操作的结果会因文件或目录在服务器中的状态变化而变化.
+ *
+ * 与 [File] 类似, [RemoteFile] 是不可变的. [renameTo] 和 [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性.
+ *
+ * ## 文件操作
+ *
+ * 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件.
+ *
+ * 示例:
+ * ```
+ * val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例
+ * val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例
+ *
+ *
+ * val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载
+ *
+ *
+ * val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息.
+ * group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送.
+ *
+ * file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本.
+ *
+ *
+ * // 要直接上传文件, 也可以简单地使用任一:
+ * group.uploadFile("/foo.txt", resource) // Kotlin
+ * resource.uploadAsFileTo(group, "/foo.txt") // Kotlin
+ * FileSupported.uploadFile(group, "/foo.txt", resource"); // Java
+ * ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java
+ * ```
+ *
+ * ## 目录操作
+ * [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录.
+ * ```
+ * val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例
+ *
+ * if (dir.exists()) { // 判断目录是否存在
+ *   // ...
+ * }
+ *
+ * dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表.
+ * dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表.
+ * ```
+ *
+ * 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" 和 "/xxx/foo.txt", 而 "/xxx/xxx/foo.txt" 不受支持.
+ *
+ * ## 文件名和目录名可重复
+ *
+ * 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠.
+ *
+ * 这个特性带来的行为有:
+ * - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件.
+ * - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器.
+ *
+ * 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id].
+ *
+ * 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` 的 [id].
+ * 服务器可以通过 [id] 准确定位重名文件中的某一个.
+ * 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件.
+ *
+ * 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件.
+ *
+ * @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口.
+ * @see FileSupported
+ * @since 2.5
+ */
+@Deprecated(
+    "Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
+    level = DeprecationLevel.WARNING
+) // deprecated since 2.8.0-RC
+@DeprecatedSinceMirai(warningSince = "2.8")
+@NotStableForInheritance
+public actual interface RemoteFile {
+    /**
+     * 文件名或目录名.
+     */
+    public actual val name: String
+
+    /**
+     * 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
+     */
+    public actual val id: String?
+
+    /**
+     * 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`.
+     *
+     * 根目录路径为 [ROOT_PATH]
+     */
+    public actual val path: String
+
+    /**
+     * 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null`
+     */
+    public actual val parent: RemoteFile?
+
+    /**
+     * 此文件所属的群或好友
+     */
+    public actual val contact: FileSupported
+
+    /**
+     * 当 [RemoteFile] 表示一个文件时返回 `true`.
+     */
+    public actual suspend fun isFile(): Boolean
+
+    /**
+     * 当 [RemoteFile] 表示一个目录时返回 `true`.
+     */
+    public actual suspend fun isDirectory(): Boolean = !isFile()
+
+    /**
+     * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定.
+     */
+    public actual suspend fun length(): Long
+
+    public actual class FileInfo @MiraiInternalApi actual constructor(
+        /**
+         * 文件或目录名.
+         */
+        public actual val name: String,
+        /**
+         * 唯一识别标识.
+         */
+        public actual val id: String,
+        /**
+         * 标准绝对路径.
+         */
+        public actual val path: String,
+        /**
+         * 文件长度 (大小) bytes, 目录的 [length] 为 0.
+         */
+        public actual val length: Long,
+        /**
+         * 下载次数. 目录没有下载次数, 此属性总是 `0`.
+         */
+        public actual val downloadTimes: Int,
+        /**
+         * 上传者 ID. 目录没有上传者, 此属性总是 `0`.
+         */
+        public actual val uploaderId: Long,
+        /**
+         * 上传的时间. 目录没有上传时间, 此属性总是 `0`.
+         */
+        public actual val uploadTime: Long,
+        /**
+         * 上次修改时间. 时间戳秒.
+         */
+        public actual val lastModifyTime: Long,
+        public actual val sha1: ByteArray,
+        public actual val md5: ByteArray,
+    ) {
+        /**
+         * 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile].
+         */
+        public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
+            contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
+    }
+
+    /**
+     * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
+     */
+    public actual suspend fun getInfo(): FileInfo?
+
+    /**
+     * 当文件或目录存在时返回 `true`.
+     */
+    public actual suspend fun exists(): Boolean
+
+    /**
+     * @return [path]
+     */
+    public actual override fun toString(): String
+
+    ///////////////////////////////////////////////////////////////////////////
+    // resolve
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录.
+     *
+     * @param relative  相对路径. 当初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolve stdlib 内的类似函数
+     */
+    public actual fun resolve(relative: String): RemoteFile
+
+    /**
+     * 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
+     *
+     * @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolve stdlib 内的类似函数
+     */
+    public actual fun resolve(relative: RemoteFile): RemoteFile
+
+    /**
+     * 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
+     * @see resolve
+     */
+    public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
+
+    /**
+     * 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null`
+     * @see resolve
+     */
+    public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
+
+    /**
+     * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
+     * 不会检查 [RemoteFile] 是否表示一个目录.
+     *
+     * @param relative 当初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolveSibling stdlib 内的类似函数
+     */
+    public actual fun resolveSibling(relative: String): RemoteFile
+
+    /**
+     * 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
+     * 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
+     *
+     * @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
+     * @see File.resolveSibling stdlib 内的类似函数
+     */
+    public actual fun resolveSibling(relative: RemoteFile): RemoteFile
+
+    ///////////////////////////////////////////////////////////////////////////
+    // operations
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     */
+    public actual suspend fun delete(): Boolean
+
+    /**
+     * 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
+     * 操作非 Bot 自己上传的文件时需要管理员权限.
+     *
+     * [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     */
+    public actual suspend fun renameTo(name: String): Boolean
+
+    /**
+     * 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     *
+     * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     *
+     * **注意**: 与 [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如:
+     * ```
+     * val root = group.filesRoot
+     * root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt"
+     * root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同.
+
+     * root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”,相当于重命名文件
+     * ```
+     *
+     * @param target 目标文件位置.
+     */
+    public actual suspend fun moveTo(target: RemoteFile): Boolean
+
+    /**
+     * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
+     *
+     * [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
+     *
+     * **已弃用:** 当 [path] 是绝对路径时, 这个函数运行正常;
+     * 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败.
+     *
+     * 使用参数为 [RemoteFile] 的 [moveTo] 代替.
+     *
+     * @suppress 在 2.6 弃用. 请使用 [moveTo]
+     */
+    @Deprecated(
+        "Use moveTo(RemoteFile) instead.",
+        replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"),
+        level = DeprecationLevel.ERROR
+    ) // deprecated since 2.7
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
+    public actual suspend fun moveTo(path: String): Boolean {
+        // Impl notes:
+        // if `path` is absolute, this works as intended.
+        // if not, `resolve(path)` will be a child path from this dir and fails always.
+        return moveTo(resolve(path))
+    }
+
+    /**
+     * 创建目录. 目录已经存在或无管理员权限时返回 `false`.
+     *
+     * 创建后 [isDirectory] 也不一定会返回 `true`.
+     * 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器.
+     */
+    public actual suspend fun mkdir(): Boolean
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow].
+     *
+     * 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询.
+     */
+    public actual suspend fun listFiles(): Flow<RemoteFile>
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器.
+     * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表.
+     */
+    @JavaFriendlyAPI
+    public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
+
+    /**
+     * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList].
+     */
+    public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
+
+    /**
+     * 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
+     */
+    public actual suspend fun toMessage(): FileMessage?
+
+    ///////////////////////////////////////////////////////////////////////////
+    // upload & download
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * 上传进度回调, 可供前端使用, 以提供进度显示.
+     * @see asProgressionCallback
+     */
+    @Deprecated(
+        "Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile",
+        ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
+        level = DeprecationLevel.WARNING
+    ) // deprecated since 2.8.0-RC
+    @DeprecatedSinceMirai(warningSince = "2.8")
+    public actual interface ProgressionCallback {
+        /**
+         * 当上传开始时调用
+         */
+        public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
+
+        /**
+         * 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
+         *
+         * 提示: 可通过 [ExternalResource.size] 获取文件总大小.
+         */
+        public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
+
+        /**
+         * 当上传成功时调用
+         */
+        public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
+
+        /**
+         * 当上传以异常失败时调用
+         */
+        public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
+
+        public actual companion object {
+            /**
+             * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
+             *
+             * 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer]到 [SendChannel] 中.
+             * 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存.
+             *
+             * 若 [closeOnFinish] 为 `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel].
+             *
+             * 使用示例:
+             * ```
+             * val progress = Channel<Long>(Channel.BUFFERED)
+             *
+             * launch {
+             *   // 每 3 秒发送一次上传进度百分比
+             *   progress.receiveAsFlow().sample(3.seconds).collect { bytes ->
+             *     group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数
+             *   }
+             * }
+             *
+             * group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true))
+             * group.sendMessage("File uploaded successfully.")
+             * ```
+             *
+             * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
+             */
+            public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
+                return object : ProgressionCallback {
+                    override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
+                        trySend(downloadedSize)
+                    }
+
+                    override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
+                        if (closeOnFinish) this@asProgressionCallback.close()
+                    }
+
+                    override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
+                        if (closeOnFinish) this@asProgressionCallback.close(exception)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度.
+     *
+     * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件.
+     * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend].
+     *
+     * ## 已弃用
+     *
+     * 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息.
+     * 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作.
+     * 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替.
+     *
+     * 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250)
+     *
+     *
+     * **注意**: [resource] 仅表示资源数据, 而不带有文件名属性.
+     * 与 [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例:
+     * ```
+     * group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录.
+     * group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录.
+     *
+     * val root = group.filesRoot
+     * root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt".
+     * root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt".
+     * ```
+     *
+     * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+     * @param callback 进度回调
+     * @throws IllegalStateException 该文件上传失败或权限不足时抛出
+     */
+    @Deprecated(
+        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR
+    ) // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public actual suspend fun upload(
+        resource: ExternalResource,
+        callback: ProgressionCallback?,
+    ): FileMessage
+
+    /**
+     * 上传文件到 [RemoteFile.path] 表示的路径.
+     * ## 已弃用
+     * 阅读 [upload] 获取更多信息
+     * @see upload
+     */
+    @Suppress("DEPRECATION_ERROR")
+    @Deprecated(
+        "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
+    )  // deprecated since 2.7-M1
+    @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+    public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
+
+    /**
+     * 上传文件并发送文件消息.
+     *
+     * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件.
+     * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件,
+     * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件.
+     *
+     * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+     * @see upload
+     */
+    @MiraiExperimentalApi
+    public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
+
+    /**
+     * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
+     */
+    public actual suspend fun getDownloadInfo(): DownloadInfo?
+
+    public actual class DownloadInfo @MiraiInternalApi actual constructor(
+        /**
+         * @see RemoteFile.name
+         */
+        public actual val filename: String,
+        /**
+         * @see RemoteFile.id
+         */
+        public actual val id: String,
+        /**
+         * 标准绝对路径
+         * @see RemoteFile.path
+         */
+        public actual val path: String,
+        /**
+         * HTTP or HTTPS URL
+         */
+        public actual val url: String,
+        public actual val sha1: ByteArray,
+        public actual val md5: ByteArray,
+    ) {
+        actual override fun toString(): String {
+            return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
+                    "md5=${md5.toUHexString("")})"
+        }
+    }
+
+    public actual companion object {
+        /**
+         * 根目录路径
+         * @see RemoteFile.path
+         */
+        public actual const val ROOT_PATH: String = "/"
+
+        /**
+         * 上传文件并获取文件消息, 但不发送.
+         *
+         * ## 已弃用
+         * 在 [upload] 获取更多信息
+         *
+         * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt'
+         * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+         * @see RemoteFile.upload
+         */
+        @Deprecated(
+            "Use sendFile instead.",
+            ReplaceWith(
+                "this.sendFile(path, resource, callback)",
+                "net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
+            ),
+            level = DeprecationLevel.ERROR
+        ) // deprecated since 2.7-M1
+        @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
+        public actual suspend fun FileSupported.uploadFile(
+            path: String,
+            resource: ExternalResource,
+            callback: ProgressionCallback?,
+        ): FileMessage =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
+
+        /**
+         * 上传文件并发送文件消息到相关 [FileSupported].
+         * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
+         * @see RemoteFile.uploadAndSend
+         */
+        @Deprecated(
+            "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
+            ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
+            level = DeprecationLevel.WARNING
+        ) // deprecated since 2.8.0-RC
+        @DeprecatedSinceMirai(warningSince = "2.8")
+        public actual suspend fun <C : FileSupported> C.sendFile(
+            path: String,
+            resource: ExternalResource,
+            callback: ProgressionCallback?,
+        ): MessageReceipt<C> =
+            @Suppress("DEPRECATION", "DEPRECATION_ERROR")
+            this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
+    }
+}

+ 3 - 0
mirai-core-utils/src/commonMain/kotlin/Arrays.kt

@@ -12,6 +12,9 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
 public inline fun <A, reified B> Array<A>.mapToArray(block: (element: A) -> B): Array<B> {
     val result = arrayOfNulls<B>(size)
     this.forEachIndexed { index, element ->

+ 1 - 4
mirai-core-utils/src/commonMain/kotlin/ByteArrayPool.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -7,9 +7,6 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:JvmMultifileClass
-@file:JvmName("MiraiUtils")
-
 package net.mamoe.mirai.utils
 
 import kotlinx.io.pool.DefaultPool

+ 4 - 0
mirai-core-utils/src/commonMain/kotlin/Bytes.kt

@@ -16,6 +16,10 @@ package net.mamoe.mirai.utils
 import kotlinx.io.core.ByteReadPacket
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmOverloads
+import kotlin.jvm.JvmSynthetic
 
 
 @JvmOverloads

+ 2 - 1
mirai-core-utils/src/commonMain/kotlin/CheckableResult.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -11,6 +11,7 @@ package net.mamoe.mirai.utils
 
 import kotlinx.serialization.Serializable
 import net.mamoe.mirai.utils.Either.Companion.fold
+import kotlin.jvm.JvmName
 import kotlin.reflect.KType
 
 @Suppress("PropertyName")

+ 17 - 0
mirai-core-utils/src/commonMain/kotlin/Closeable.kt

@@ -0,0 +1,17 @@
+/*
+ * 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.utils
+
+import kotlinx.io.errors.IOException
+
+public expect interface Closeable {
+    @Throws(IOException::class)
+    public fun close()
+}

+ 13 - 0
mirai-core-utils/src/commonMain/kotlin/Collections.kt

@@ -0,0 +1,13 @@
+/*
+ * 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.utils
+
+@Suppress("FunctionName")
+public expect fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V>

+ 8 - 5
mirai-core-utils/src/commonMain/kotlin/ComputeOnNullMutableProperty.kt

@@ -1,15 +1,17 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.utils
 
 import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.locks.SynchronizedObject
+import kotlinx.atomicfu.locks.synchronized
 import kotlin.reflect.KProperty
 
 public fun <T : Any> computeOnNullMutableProperty(initializer: () -> T): ComputeOnNullMutableProperty<T> =
@@ -28,10 +30,11 @@ private class ComputeOnNullMutablePropertyImpl<T : Any>(
     private val initializer: () -> T
 ) : ComputeOnNullMutableProperty<T> {
     private val value = atomic<T?>(null)
+    private val lock = SynchronizedObject()
 
     override tailrec fun get(): T {
         return when (val v = this.value.value) {
-            null -> synchronized(this) {
+            null -> synchronized(lock) {
                 if (this.value.value === null) {
                     val value = this.initializer()
                     // compiler inserts

+ 3 - 0
mirai-core-utils/src/commonMain/kotlin/Conversions.kt

@@ -14,6 +14,9 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
 /*
  * 类型转换 Utils.
  * 这些函数为内部函数, 可能会改变

+ 7 - 17
mirai-core-utils/src/commonMain/kotlin/CoroutineUtils.kt

@@ -7,8 +7,8 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
-@file:JvmMultifileClass
-@file:JvmName("MiraiUtils")
+
+@file:JvmName("CoroutineUtils_common")
 
 package net.mamoe.mirai.utils
 
@@ -17,25 +17,15 @@ import kotlinx.coroutines.sync.Semaphore
 import kotlinx.coroutines.sync.withPermit
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.jvm.JvmName
 
-@Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "DeprecatedCallableAddReplaceWith")
-@Deprecated(
-    message = "Use runBIO which delegates to `runInterruptible`. " +
-            "Technically remove suspend call in `block` and remove CoroutineScope parameter usages.",
-    level = DeprecationLevel.HIDDEN
-)
-@kotlin.internal.LowPriorityInOverloadResolution
-public suspend inline fun <R> runBIO(
-    noinline block: suspend CoroutineScope.() -> R,
-): R = withContext(Dispatchers.IO, block)
-
-public suspend inline fun <R> runBIO(
+public expect suspend inline fun <R> runBIO(
     noinline block: () -> R,
-): R = runInterruptible(context = Dispatchers.IO, block = block)
+): R
 
-public suspend inline fun <T, R> T.runBIO(
+public expect suspend inline fun <T, R> T.runBIO(
     crossinline block: T.() -> R,
-): R = runInterruptible(context = Dispatchers.IO, block = { block() })
+): R
 
 public inline fun CoroutineScope.launchWithPermit(
     semaphore: Semaphore,

+ 4 - 0
mirai-core-utils/src/commonMain/kotlin/Either.kt

@@ -11,6 +11,10 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmField
+import kotlin.jvm.JvmInline
+import kotlin.jvm.JvmName
+
 /**
  * Safe union of two types.
  */

+ 7 - 12
mirai-core-utils/src/commonMain/kotlin/ExceptionCollector.kt

@@ -11,6 +11,8 @@ package net.mamoe.mirai.utils
 
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.jvm.Synchronized
+import kotlin.jvm.Volatile
 
 public open class ExceptionCollector {
 
@@ -50,16 +52,6 @@ public open class ExceptionCollector {
         receiver.addSuppressed(e)
     }
 
-    private fun hash(e: Throwable): Long {
-        return e.stackTrace.fold(0L) { acc, stackTraceElement ->
-            acc * 31 + hash(stackTraceElement).toLongUnsigned()
-        }
-    }
-
-    private fun hash(element: StackTraceElement): Int {
-        return element.lineNumber.hashCode() xor element.className.hashCode() xor element.methodName.hashCode()
-    }
-
     public fun collectGet(e: Throwable?): Throwable {
         this.collect(e)
         return getLast()!!
@@ -90,7 +82,8 @@ public open class ExceptionCollector {
     @TestOnly // very slow
     public fun asSequence(): Sequence<Throwable> {
         fun Throwable.itr(): Iterator<Throwable> {
-            return (sequenceOf(this) + this.suppressed.asSequence().flatMap { it.itr().asSequence() }).iterator()
+            return (sequenceOf(this) + this.suppressedExceptions.asSequence()
+                .flatMap { it.itr().asSequence() }).iterator()
         }
 
         val last = getLast() ?: return emptySequence()
@@ -136,4 +129,6 @@ public inline fun <R> ExceptionCollector.withExceptionCollector(action: Exceptio
             collectThrow(e)
         }
     }
-}
+}
+
+internal expect fun hash(e: Throwable): Long

+ 7 - 4
mirai-core-utils/src/commonMain/kotlin/Files.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass
@@ -12,6 +12,9 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
 /**
  * 文件头和文件类型列表
  */

+ 8 - 24
mirai-core-utils/src/commonMain/kotlin/IO.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass
@@ -15,9 +15,11 @@
 package net.mamoe.mirai.utils
 
 import kotlinx.io.charsets.Charset
+import kotlinx.io.charsets.Charsets
 import kotlinx.io.core.*
-import java.io.File
-import kotlin.text.String
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmSynthetic
 
 public val EMPTY_BYTE_ARRAY: ByteArray = ByteArray(0)
 
@@ -132,21 +134,3 @@ public inline fun Input.readString(length: Byte, charset: Charset = Charsets.UTF
 
 public fun Input.readUShortLVString(): String = String(this.readUShortLVByteArray())
 public fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().toInt())
-
-public fun File.createFileIfNotExists() {
-    if (!this.exists()) {
-        this.parentFile.mkdirs()
-        this.createNewFile()
-    }
-}
-
-public fun File.resolveCreateFile(relative: String): File = this.resolve(relative).apply { createFileIfNotExists() }
-public fun File.resolveCreateFile(relative: File): File = this.resolve(relative).apply { createFileIfNotExists() }
-
-public fun File.resolveMkdir(relative: String): File = this.resolve(relative).apply { mkdirs() }
-public fun File.resolveMkdir(relative: File): File = this.resolve(relative).apply { mkdirs() }
-
-public fun File.touch(): File = apply {
-    parentFile?.mkdirs()
-    createNewFile()
-}

+ 8 - 6
mirai-core-utils/src/commonMain/kotlin/LateinitMutableProperty.kt

@@ -1,15 +1,17 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.utils
 
 import kotlinx.atomicfu.atomic
+import kotlinx.atomicfu.locks.SynchronizedObject
+import kotlinx.atomicfu.locks.synchronized
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
 
@@ -24,7 +26,7 @@ public fun <T> lateinitMutableProperty(initializer: () -> T): ReadWriteProperty<
 
 private class LateinitMutableProperty<T>(
     initializer: () -> T
-) : ReadWriteProperty<Any?, T> {
+) : ReadWriteProperty<Any?, T>, SynchronizedObject() {
     private val value = atomic(UNINITIALIZED)
 
     private var initializer: (() -> T)? = initializer
@@ -39,7 +41,7 @@ private class LateinitMutableProperty<T>(
                     this.initializer = null
                     this.value.compareAndSet(UNINITIALIZED, value) // setValue prevails
                     this.value.value.let {
-                        assert(it !== UNINITIALIZED)
+                        check(it !== UNINITIALIZED)
                         return it as T
                     }
                 } else this.value.value as T

+ 18 - 110
mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass
@@ -12,117 +12,39 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.io.core.Input
-import kotlinx.io.core.readAvailable
-import java.io.*
-import java.net.Inet4Address
-import java.security.MessageDigest
-import java.util.zip.Deflater
-import java.util.zip.GZIPInputStream
-import java.util.zip.GZIPOutputStream
-import java.util.zip.Inflater
+import kotlinx.io.core.Closeable
+import kotlinx.io.core.toByteArray
+import kotlinx.io.core.use
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
 
-@JvmOverloads
-public fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray {
-    checkOffsetAndLength(offset, length)
-    if (length == 0) return ByteArray(0)
-
-    val inflater = Inflater()
-    inflater.reset()
-    ByteArrayOutputStream().use { output ->
-        inflater.setInput(this, offset, length)
-        ByteArray(DEFAULT_BUFFER_SIZE).let {
-            while (!inflater.finished()) {
-                output.write(it, 0, inflater.inflate(it))
-            }
-        }
-
-        inflater.end()
-        return output.toByteArray()
-    }
-}
+public expect val DEFAULT_BUFFER_SIZE: Int
 
-public fun InputStream.md5(): ByteArray {
-    return digest("md5")
-}
+public expect fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray
 
-public fun InputStream.digest(algorithm: String): ByteArray {
-    val digest = MessageDigest.getInstance(algorithm)
-    digest.reset()
-    use { input ->
-        object : OutputStream() {
-            override fun write(b: Int) {
-                digest.update(b.toByte())
-            }
-
-            override fun write(b: ByteArray, off: Int, len: Int) {
-                digest.update(b, off, len)
-            }
-        }.use { output ->
-            input.copyTo(output)
-        }
-    }
-    return digest.digest()
-}
-
-public fun InputStream.sha1(): ByteArray {
-    return digest("SHA-1")
-}
 
 /**
  * Localhost 解析
  */
-public fun localIpAddress(): String = runCatching {
-    Inet4Address.getLocalHost().hostAddress
-}.getOrElse { "192.168.1.123" }
+public expect fun localIpAddress(): String
 
 public fun String.md5(): ByteArray = toByteArray().md5()
 
-@JvmOverloads
-public fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray {
-    checkOffsetAndLength(offset, length)
-    return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
-}
+public expect fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray
 
 public fun String.sha1(): ByteArray = toByteArray().sha1()
 
-@JvmOverloads
-public fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray {
-    checkOffsetAndLength(offset, length)
-    return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest()
-}
-
-@JvmOverloads
-public fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray {
-    return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() }
-}
+public expect fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray
 
-@JvmOverloads
-public fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray {
-    ByteArrayOutputStream().use { buf ->
-        GZIPOutputStream(buf).use { gzip ->
-            inputStream(offset, length).use { t -> t.copyTo(gzip) }
-        }
-        buf.flush()
-        return buf.toByteArray()
-    }
-}
+public expect fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray
 
-@JvmOverloads
-public fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray {
-    checkOffsetAndLength(offset, length)
-    if (length == 0) return ByteArray(0)
+public expect fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray
 
-    val deflater = Deflater()
-    deflater.setInput(this, offset, length)
-    deflater.finish()
+public expect fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray
 
-    ByteArray(DEFAULT_BUFFER_SIZE).let {
-        return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
-    }
-}
+public expect fun availableProcessors(): Int
 
 public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R {
     contract {
@@ -131,20 +53,6 @@ public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R {
     return use(block)
 }
 
-@Throws(IOException::class)
-@JvmOverloads
-public fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
-    var bytesCopied: Long = 0
-    val buffer = ByteArray(bufferSize)
-    var bytes = readAvailable(buffer)
-    while (bytes >= 0) {
-        out.write(buffer, 0, bytes)
-        bytesCopied += bytes
-        bytes = readAvailable(buffer)
-    }
-    return bytesCopied
-}
-
 public inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
     contract {
         callsInPlace(block, InvocationKind.EXACTLY_ONCE)

+ 3 - 0
mirai-core-utils/src/commonMain/kotlin/Numbers.kt

@@ -12,6 +12,9 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+
 public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF)
 public fun Short.toIntUnsigned(): Int = this.toUShort().toInt()
 public fun Byte.toIntUnsigned(): Int = toInt() and 0xFF

+ 6 - 4
mirai-core-utils/src/commonMain/kotlin/RandomUtils.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass
@@ -14,6 +14,8 @@
 package net.mamoe.mirai.utils
 
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
 import kotlin.math.absoluteValue
 import kotlin.random.Random
 import kotlin.random.nextInt

+ 11 - 17
mirai-core-utils/src/commonMain/kotlin/Resources.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -9,13 +9,7 @@
 
 package net.mamoe.mirai.utils
 
-import java.util.concurrent.atomic.AtomicInteger
-
-
-@TestOnly
-public fun readResource(url: String): String =
-    Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString()
-        ?: error("Could not find resource '$url'")
+import kotlinx.atomicfu.atomic
 
 public class ResourceAccessLock {
     public companion object {
@@ -27,7 +21,7 @@ public class ResourceAccessLock {
     /*
      * status > 0  ->  Number of holders using resource
      */
-    private val status = AtomicInteger(-1)
+    private val status = atomic(-1)
 
     /**
      * ```
@@ -54,7 +48,7 @@ public class ResourceAccessLock {
     public fun tryUse(): Boolean {
         val c = status
         while (true) {
-            val v = c.get()
+            val v = c.value
             if (v < 0) return false
             if (c.compareAndSet(v, v + 1)) return true
         }
@@ -63,7 +57,7 @@ public class ResourceAccessLock {
     public fun lockIfNotUsing(): Boolean {
         val count = this.status
         while (true) {
-            val value = count.get()
+            val value = count.value
             if (value != 0) return false
             if (count.compareAndSet(0, -2)) return true
         }
@@ -72,7 +66,7 @@ public class ResourceAccessLock {
     public fun release() {
         val count = this.status
         while (true) {
-            val value = count.get()
+            val value = count.value
             if (value < 1) throw IllegalStateException("Current resource not in using")
 
             if (count.compareAndSet(value, value - 1)) return
@@ -84,11 +78,11 @@ public class ResourceAccessLock {
     }
 
     public fun setInitialized() {
-        status.set(INITIALIZED)
+        status.value = INITIALIZED
     }
 
     public fun setLocked() {
-        status.set(LOCKED)
+        status.value = LOCKED
     }
 
     public fun setDisposed() {
@@ -96,13 +90,13 @@ public class ResourceAccessLock {
     }
 
     public fun setUninitialized() {
-        status.set(UNINITIALIZED)
+        status.value = UNINITIALIZED
     }
 
-    public fun currentStatus(): Int = status.get()
+    public fun currentStatus(): Int = status.value
 
     override fun toString(): String {
-        return when (val status = status.get()) {
+        return when (val status = status.value) {
             0 -> "ResourceAccessLock(INITIALIZED)"
             -1 -> "ResourceAccessLock(UNINITIALIZED)"
             -2 -> "ResourceAccessLock(LOCKED)"

+ 6 - 4
mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 
@@ -13,6 +13,8 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 
 

+ 0 - 29
mirai-core-utils/src/commonMain/kotlin/Serialization.kt

@@ -7,41 +7,12 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:JvmMultifileClass
-@file:JvmName("MiraiUtils")
-
-
 package net.mamoe.mirai.utils
 
-import kotlinx.serialization.BinaryFormat
-import kotlinx.serialization.DeserializationStrategy
 import kotlinx.serialization.KSerializer
-import kotlinx.serialization.StringFormat
 import kotlinx.serialization.descriptors.*
 import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
-import java.io.File
-
-public fun <T> File.loadNotBlankAs(
-    serializer: DeserializationStrategy<T>,
-    stringFormat: StringFormat,
-): T? {
-    if (!this.exists() || this.length() == 0L) {
-        return null
-    }
-    return stringFormat.decodeFromString(serializer, this.readText())
-}
-
-public fun <T> File.loadNotBlankAs(
-    serializer: DeserializationStrategy<T>,
-    binaryFormat: BinaryFormat,
-): T? {
-    if (!this.exists() || this.length() == 0L) {
-        return null
-    }
-    return binaryFormat.decodeFromByteArray(serializer, this.readBytes())
-}
-
 
 public fun SerialDescriptor.copy(newName: String): SerialDescriptor =
     buildClassSerialDescriptor(newName) { takeElementsFrom(this@copy) }

+ 4 - 3
mirai-core-utils/src/commonMain/kotlin/SizedCache.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -9,12 +9,13 @@
 
 package net.mamoe.mirai.utils
 
+import kotlinx.atomicfu.locks.ReentrantLock
+import kotlinx.atomicfu.locks.reentrantLock
 import kotlinx.atomicfu.locks.withLock
-import java.util.concurrent.locks.ReentrantLock
 
 @Suppress("unused", "UNCHECKED_CAST")
 public class SizedCache<T>(size: Int) : Iterable<T> {
-    public val lock: ReentrantLock = ReentrantLock()
+    public val lock: ReentrantLock = reentrantLock()
     public val data: Array<Any?> = arrayOfNulls(size)
 
     public var filled: Boolean = false

+ 22 - 33
mirai-core-utils/src/commonMain/kotlin/StandardUtils.kt

@@ -7,14 +7,13 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:JvmMultifileClass
-@file:JvmName("MiraiUtils")
+@file:JvmName("StandardUtilsKt_common")
 
 package net.mamoe.mirai.utils
 
-import java.util.*
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
+import kotlin.jvm.JvmName
 
 public inline fun <reified T> Any?.cast(): T {
     contract { returns() implies (this@cast is T) }
@@ -58,34 +57,6 @@ public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) {
     }
 }
 
-public fun <T> Collection<T>.asImmutable(): Collection<T> {
-    return when (this) {
-        is List<T> -> asImmutable()
-        is Set<T> -> asImmutable()
-        else -> Collections.unmodifiableCollection(this)
-    }
-}
-
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <T> Collection<T>.asImmutableStrict(): Collection<T> {
-    return Collections.unmodifiableCollection(this)
-}
-
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <T> List<T>.asImmutable(): List<T> {
-    return Collections.unmodifiableList(this)
-}
-
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <T> Set<T>.asImmutable(): Set<T> {
-    return Collections.unmodifiableSet(this)
-}
-
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <K, V> Map<K, V>.asImmutable(): Map<K, V> {
-    return Collections.unmodifiableMap(this)
-}
-
 public fun Throwable.getRootCause(maxDepth: Int = 20): Throwable {
     var depth = 0
     var rootCause: Throwable? = this
@@ -156,7 +127,7 @@ public inline fun Throwable.findCauseOrSelf(maxDepth: Int = 20, filter: (Throwab
     findCause(maxDepth, filter) ?: this
 
 public fun String.capitalize(): String {
-    return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
+    return replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
 }
 
 public fun String.truncated(length: Int, truncated: String = "..."): String {
@@ -177,4 +148,22 @@ public inline fun <T> T.context(block: T.() -> Unit) {
 }
 
 public fun assertUnreachable(hint: String? = null): Nothing =
-    error("This clause should not be reached. " + hint.orEmpty())
+    error("This clause should not be reached. " + hint.orEmpty())
+
+public fun isSameClass(object1: Any?, object2: Any?): Boolean {
+    if (object1 == null || object2 == null) {
+        return object1 == null && object2 == null
+    }
+    return isSameClassPlatform(object1, object2)
+}
+
+internal expect fun isSameClassPlatform(object1: Any, object2: Any): Boolean
+
+public inline fun <reified T> isSameType(thisObject: T, other: Any?): Boolean {
+    contract {
+        returns() implies (other is T)
+    }
+    if (other == null) return false
+    if (other !is T) return false
+    return isSameClass(thisObject, other)
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 11 - 0
mirai-core-utils/src/commonMain/kotlin/Strings.kt


+ 6 - 4
mirai-core-utils/src/commonMain/kotlin/Symbol.kt

@@ -1,14 +1,16 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmName
+
 public class Symbol private constructor(name: String) {
     private val str = "Symbol($name)"
     override fun toString(): String = str

+ 8 - 10
mirai-core-utils/src/commonMain/kotlin/TimeUtils.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass
@@ -12,17 +12,15 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
+import kotlin.jvm.JvmSynthetic
 import kotlin.math.floor
 import kotlin.time.Duration
 import kotlin.time.DurationUnit
 import kotlin.time.ExperimentalTime
 
-/**
- * 时间戳
- *
- * @see System.currentTimeMillis
- */
-public fun currentTimeMillis(): Long = System.currentTimeMillis()
+public expect fun currentTimeMillis(): Long
 
 /**
  * 时间戳到秒

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/TypeSafeMap.kt

@@ -12,12 +12,12 @@
 package net.mamoe.mirai.utils
 
 import kotlinx.serialization.Serializable
-import java.util.concurrent.ConcurrentHashMap
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 import kotlin.properties.ReadOnlyProperty
 import kotlin.properties.ReadWriteProperty
 import kotlin.reflect.KProperty
+import kotlin.jvm.JvmInline
 
 @Serializable
 @JvmInline

+ 4 - 2
mirai-core-utils/src/commonMain/kotlin/UnsafeMutableNonNullProperty.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -9,6 +9,8 @@
 
 package net.mamoe.mirai.utils
 
+import kotlin.jvm.JvmField
+
 public fun <T : Any> unsafeMutableNonNullPropertyOf(
     name: String = "<unknown>"
 ): UnsafeMutableNonNullProperty<T> {
@@ -24,7 +26,7 @@ public class UnsafeMutableNonNullProperty<T : Any>(
 
     public val isInitialized: Boolean get() = value0 !== null
     public var value: T
-        get() = value0 ?: throw UninitializedPropertyAccessException("Property `$propertyName` not initialized")
+        get() = value0 ?: throw IllegalStateException("Property `$propertyName` not initialized")
         set(value) {
             value0 = value
         }

+ 10 - 8
mirai-core-utils/src/commonMain/kotlin/systemProp.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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
  */
 
 
@@ -13,18 +13,20 @@
 
 package net.mamoe.mirai.utils
 
-import java.util.concurrent.ConcurrentHashMap
+import kotlin.jvm.JvmMultifileClass
+import kotlin.jvm.JvmName
 
+internal expect fun getProperty(name: String, default: String): String?
 
 public fun systemProp(name: String, default: String): String =
-    System.getProperty(name, default) ?: default
+    getProperty(name, default) ?: default
 
 public fun systemProp(name: String, default: Boolean): Boolean =
-    System.getProperty(name, default.toString())?.toBoolean() ?: default
+    getProperty(name, default.toString())?.toBoolean() ?: default
 
 
 public fun systemProp(name: String, default: Long): Long =
-    System.getProperty(name, default.toString())?.toLongOrNull() ?: default
+    getProperty(name, default.toString())?.toLongOrNull() ?: default
 
 
 private val debugProps = ConcurrentHashMap<String, Boolean>()

+ 12 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/Closeable.kt

@@ -0,0 +1,12 @@
+/*
+ * 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.utils
+
+public actual typealias Closeable = java.io.Closeable

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

@@ -0,0 +1,46 @@
+/*
+ * 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.utils
+
+import java.util.*
+
+
+public fun <T> Collection<T>.asImmutable(): Collection<T> {
+    return when (this) {
+        is List<T> -> asImmutable()
+        is Set<T> -> asImmutable()
+        else -> Collections.unmodifiableCollection(this)
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> Collection<T>.asImmutableStrict(): Collection<T> {
+    return Collections.unmodifiableCollection(this)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> List<T>.asImmutable(): List<T> {
+    return Collections.unmodifiableList(this)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <T> Set<T>.asImmutable(): Set<T> {
+    return Collections.unmodifiableSet(this)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+public inline fun <K, V> Map<K, V>.asImmutable(): Map<K, V> {
+    return Collections.unmodifiableMap(this)
+}
+
+@Suppress("FunctionName")
+public actual fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V> {
+    return java.util.concurrent.ConcurrentHashMap()
+}

+ 21 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/CoroutineUtils.kt

@@ -0,0 +1,21 @@
+/*
+ * 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.utils
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runInterruptible
+
+public actual suspend inline fun <R> runBIO(
+    noinline block: () -> R,
+): R = runInterruptible(context = Dispatchers.IO, block = block)
+
+public actual suspend inline fun <T, R> T.runBIO(
+    crossinline block: T.() -> R,
+): R = runInterruptible(context = Dispatchers.IO, block = { block() })

+ 120 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/Crypto.kt

@@ -0,0 +1,120 @@
+/*
+ * 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:JvmMultifileClass
+@file:JvmName("MiraiUtils")
+
+package net.mamoe.mirai.utils
+
+import kotlinx.io.core.use
+import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.net.Inet4Address
+import java.security.MessageDigest
+import java.util.zip.Deflater
+import java.util.zip.GZIPInputStream
+import java.util.zip.GZIPOutputStream
+import java.util.zip.Inflater
+
+public actual val DEFAULT_BUFFER_SIZE: Int get() = kotlin.io.DEFAULT_BUFFER_SIZE
+
+public actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
+    checkOffsetAndLength(offset, length)
+    if (length == 0) return ByteArray(0)
+
+    val inflater = Inflater()
+    inflater.reset()
+    ByteArrayOutputStream().use { output ->
+        inflater.setInput(this, offset, length)
+        ByteArray(DEFAULT_BUFFER_SIZE).let {
+            while (!inflater.finished()) {
+                output.write(it, 0, inflater.inflate(it))
+            }
+        }
+
+        inflater.end()
+        return output.toByteArray()
+    }
+}
+
+public actual fun localIpAddress(): String = runCatching {
+    Inet4Address.getLocalHost().hostAddress
+}.getOrElse { "192.168.1.123" }
+
+public fun InputStream.md5(): ByteArray {
+    return digest("md5")
+}
+
+public fun InputStream.digest(algorithm: String): ByteArray {
+    val digest = MessageDigest.getInstance(algorithm)
+    digest.reset()
+    use { input ->
+        object : OutputStream() {
+            override fun write(b: Int) {
+                digest.update(b.toByte())
+            }
+
+            override fun write(b: ByteArray, off: Int, len: Int) {
+                digest.update(b, off, len)
+            }
+        }.use { output ->
+            input.copyTo(output)
+        }
+    }
+    return digest.digest()
+}
+
+public fun InputStream.sha1(): ByteArray {
+    return digest("SHA-1")
+}
+
+public actual fun ByteArray.md5(offset: Int, length: Int): ByteArray {
+    checkOffsetAndLength(offset, length)
+    return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
+}
+
+
+@JvmOverloads
+public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray {
+    checkOffsetAndLength(offset, length)
+    return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest()
+}
+
+@JvmOverloads
+public actual fun ByteArray.ungzip(offset: Int, length: Int): ByteArray {
+    return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() }
+}
+
+@JvmOverloads
+public actual fun ByteArray.gzip(offset: Int, length: Int): ByteArray {
+    ByteArrayOutputStream().use { buf ->
+        GZIPOutputStream(buf).use { gzip ->
+            inputStream(offset, length).use { t -> t.copyTo(gzip) }
+        }
+        buf.flush()
+        return buf.toByteArray()
+    }
+}
+
+@JvmOverloads
+public actual fun ByteArray.zip(offset: Int, length: Int): ByteArray {
+    checkOffsetAndLength(offset, length)
+    if (length == 0) return ByteArray(0)
+
+    val deflater = Deflater()
+    deflater.setInput(this, offset, length)
+    deflater.finish()
+
+    ByteArray(DEFAULT_BUFFER_SIZE).let {
+        return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
+    }
+}
+
+public actual fun availableProcessors(): Int = Runtime.getRuntime().availableProcessors()

+ 20 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/ExceptionCollector.kt

@@ -0,0 +1,20 @@
+/*
+ * 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.utils
+
+internal actual fun hash(e: Throwable): Long {
+    return e.stackTrace.fold(0L) { acc, stackTraceElement ->
+        acc * 31 + hash(stackTraceElement).toLongUnsigned()
+    }
+}
+
+private fun hash(element: StackTraceElement): Int {
+    return element.lineNumber.hashCode() xor element.className.hashCode() xor element.methodName.hashCode()
+}

+ 34 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/File.kt

@@ -0,0 +1,34 @@
+/*
+ * 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:JvmMultifileClass
+@file:JvmName("MiraiUtils")
+
+package net.mamoe.mirai.utils
+
+import java.io.File
+
+
+public fun File.createFileIfNotExists() {
+    if (!this.exists()) {
+        this.parentFile.mkdirs()
+        this.createNewFile()
+    }
+}
+
+public fun File.resolveCreateFile(relative: String): File = this.resolve(relative).apply { createFileIfNotExists() }
+public fun File.resolveCreateFile(relative: File): File = this.resolve(relative).apply { createFileIfNotExists() }
+
+public fun File.resolveMkdir(relative: String): File = this.resolve(relative).apply { mkdirs() }
+public fun File.resolveMkdir(relative: File): File = this.resolve(relative).apply { mkdirs() }
+
+public fun File.touch(): File = apply {
+    parentFile?.mkdirs()
+    createNewFile()
+}

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/JvmNioBuffer.kt → mirai-core-utils/src/jvmBaseMain/kotlin/JvmNioBuffer.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/Reflections.kt → mirai-core-utils/src/jvmBaseMain/kotlin/Reflections.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.

+ 16 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/Resources.kt

@@ -0,0 +1,16 @@
+/*
+ * 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.utils
+
+
+@TestOnly
+public fun readResource(url: String): String =
+    Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString()
+        ?: error("Could not find resource '$url'")

+ 1 - 2
mirai-core-utils/src/commonMain/kotlin/SecretsProtection.kt → mirai-core-utils/src/jvmBaseMain/kotlin/SecretsProtection.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
@@ -13,7 +13,6 @@ import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.builtins.ByteArraySerializer
 import kotlinx.serialization.builtins.serializer
-import kotlinx.serialization.serializer
 import java.nio.ByteBuffer
 import java.util.concurrent.ConcurrentLinkedDeque
 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater

+ 37 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/Serialization.kt

@@ -0,0 +1,37 @@
+/*
+ * 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.utils
+
+import kotlinx.serialization.BinaryFormat
+import kotlinx.serialization.DeserializationStrategy
+import kotlinx.serialization.StringFormat
+import java.io.File
+
+
+public fun <T> File.loadNotBlankAs(
+    serializer: DeserializationStrategy<T>,
+    stringFormat: StringFormat,
+): T? {
+    if (!this.exists() || this.length() == 0L) {
+        return null
+    }
+    return stringFormat.decodeFromString(serializer, this.readText())
+}
+
+public fun <T> File.loadNotBlankAs(
+    serializer: DeserializationStrategy<T>,
+    binaryFormat: BinaryFormat,
+): T? {
+    if (!this.exists() || this.length() == 0L) {
+        return null
+    }
+    return binaryFormat.decodeFromByteArray(serializer, this.readBytes())
+}
+

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/Streams.kt → mirai-core-utils/src/jvmBaseMain/kotlin/Streams.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/ThreadLocal.kt → mirai-core-utils/src/jvmBaseMain/kotlin/ThreadLocal.kt

@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.

+ 12 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/TimeUtils.kt

@@ -0,0 +1,12 @@
+/*
+ * 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.utils
+
+public actual fun currentTimeMillis(): Long = System.currentTimeMillis()

+ 4 - 4
mirai-core-utils/src/commonMain/kotlin/WeakRef.kt → mirai-core-utils/src/jvmBaseMain/kotlin/WeakRef.kt

@@ -1,10 +1,10 @@
 /*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
+ * 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.
+ * 此源代码的使用受 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:JvmMultifileClass

+ 9 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/package.kt

@@ -0,0 +1,9 @@
+/*
+ * 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.utils

+ 12 - 0
mirai-core-utils/src/jvmBaseMain/kotlin/systemProp.kt

@@ -0,0 +1,12 @@
+/*
+ * 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.utils
+
+internal actual fun getProperty(name: String, default: String): String? = System.getProperty(name, default)

+ 10 - 0
mirai-core-utils/src/jvmBaseTest/kotlin/package.kt

@@ -0,0 +1,10 @@
+/*
+ * 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.utils

+ 17 - 0
mirai-core-utils/src/nativeMain/kotlin/Closeable.kt

@@ -0,0 +1,17 @@
+/*
+ * 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.utils
+
+import kotlinx.io.errors.IOException
+
+public actual interface Closeable {
+    @Throws(IOException::class)
+    public actual fun close()
+}

+ 15 - 0
mirai-core-utils/src/nativeMain/kotlin/ConcurrentHashMap.kt

@@ -0,0 +1,15 @@
+/*
+ * 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.utils
+
+@Suppress("FunctionName")
+public actual fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V> {
+    TODO("Not yet implemented")
+}

+ 70 - 0
mirai-core-utils/src/nativeMain/kotlin/CoroutineUtils.kt

@@ -0,0 +1,70 @@
+/*
+ * 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.utils
+
+public actual suspend inline fun <R> runBIO(noinline block: () -> R): R {
+    return block()
+}
+
+public actual suspend inline fun <T, R> T.runBIO(crossinline block: T.() -> R): R {
+    TODO("Not yet implemented")
+}
+
+/**
+ * For code
+ * ```
+ * try {
+ *   job(new)
+ * } catch (e: Throwable) {
+ *   throw IllegalStateException("Exception in attached Job '$name'", e.unwrapCancellationException())
+ * }
+ * ```
+ *
+ * Original stacktrace, you mainly see `StateSwitchingException` which is useless to locate the code where real cause `ForceOfflineException` is thrown.
+ * ```
+ * Exception in thread "DefaultDispatcher-worker-1 @BotInitProcessor.init#7" java.lang.IllegalStateException: Exception in attached Job 'BotInitProcessor.init'
+ *   at net.mamoe.mirai.internal.network.handler.state.JobAttachStateObserver$stateChanged0$1.invokeSuspend(JobAttachStateObserver.kt:40)
+ *   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
+ *   at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
+ * Caused by: StateSwitchingException(old=StateLoading, new=StateClosed, cause=net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@4abf6d30)
+ *   at net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport.setStateImpl$mirai_core(NetworkHandlerSupport.kt:258)
+ *   at net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler.close(NettyNetworkHandler.kt:404)
+ * ```
+ *
+ * Real stacktrace (with [unwrapCancellationException]), you directly have `ForceOfflineException`, also you wont lose information of `StateSwitchingException`
+ * ```
+ * Exception in thread "DefaultDispatcher-worker-2 @BotInitProcessor.init#7" java.lang.IllegalStateException: Exception in attached Job 'BotInitProcessor.init'
+ *   at net.mamoe.mirai.internal.network.handler.state.JobAttachStateObserver$stateChanged0$1.invokeSuspend(JobAttachStateObserver.kt:40)
+ *   at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
+ *   at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
+ *   at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
+ * Caused by: net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@62f65f94
+ *   at net.mamoe.mirai.utils.MiraiUtils__CoroutineUtilsKt.unwrapCancellationException(CoroutineUtils.kt:141)
+ *   at net.mamoe.mirai.utils.MiraiUtils.unwrapCancellationException(Unknown Source)
+ *   ... 7 more
+ *   Suppressed: StateSwitchingException(old=StateLoading, new=StateClosed, cause=net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@62f65f94)
+ *     at net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport.setStateImpl$mirai_core(NetworkHandlerSupport.kt:258)
+ *     at net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler.close(NettyNetworkHandler.kt:404)
+ * ```
+ */
+@Suppress("unused")
+public actual inline fun <reified E> Throwable.unwrap(): Throwable {
+    if (this !is E) return this
+    return this.findCause { it !is E }
+        ?.also { it.addSuppressed(this) }
+        ?: this
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.