瀏覽代碼

Improve LoginSolver, close #703:
- Remove DefaultLoginSolver (originally experimental API)
- Add docs
- No default instance for Android platform
- LoginSolver.Default is nullable now (in case on Android platform)
- BotConfiguration.loginSolver is nullable now (meaning not provided by the user)

Him188 5 年之前
父節點
當前提交
e3b553b4de

+ 4 - 1
build.gradle.kts

@@ -141,7 +141,10 @@ fun Project.configureJvmTarget() {
     }
 
     kotlinTargets.orEmpty().filterIsInstance<KotlinJvmTarget>().forEach { target ->
-        target.compilations.all { kotlinOptions.jvmTarget = "1.8" }
+        target.compilations.all {
+            kotlinOptions.jvmTarget = "1.8"
+            kotlinOptions.languageVersion = "1.4"
+        }
         target.testRuns["test"].executionTask.configure { useJUnitPlatform() }
     }
 

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

@@ -52,7 +52,7 @@ public class RetryLaterException @MiraiInternalApi constructor() :
  * 无标准输入或 Kotlin 不支持此输入.
  */
 public class NoStandardInputForCaptchaException @MiraiInternalApi constructor(
-    public override val cause: Throwable?
+    public override val cause: Throwable? = null
 ) : LoginFailedException(true, "no standard input for captcha")
 
 /**

+ 13 - 2
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -90,8 +90,17 @@ public open class BotConfiguration { // open for Java
     /** 最多尝试多少次重连 */
     public var reconnectionRetryTimes: Int = Int.MAX_VALUE
 
-    /** 验证码处理器 */
-    public var loginSolver: LoginSolver = LoginSolver.Default
+    /**
+     * 验证码处理器
+     *
+     * - 在 Android 需要手动提供 [LoginSolver]
+     * - 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
+     *
+     * 详见 [LoginSolver.Default]
+     *
+     * @see LoginSolver
+     */
+    public var loginSolver: LoginSolver? = LoginSolver.Default
 
     /** 使用协议类型 */
     public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
@@ -115,6 +124,7 @@ public open class BotConfiguration { // open for Java
         Json {
             isLenient = true
             ignoreUnknownKeys = true
+            prettyPrint = true
         }
     }.getOrElse { Json {} }
 
@@ -133,6 +143,7 @@ public open class BotConfiguration { // open for Java
      *
      * @see deviceInfo
      */
+    @ConfigurationDsl
     public fun loadDeviceInfoJson(json: String) {
         deviceInfo = {
             this.json.decodeFromString(DeviceInfo.serializer(), json)

+ 54 - 30
mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt

@@ -20,6 +20,8 @@ import kotlinx.coroutines.withContext
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.network.LoginFailedException
 import net.mamoe.mirai.network.NoStandardInputForCaptchaException
+import net.mamoe.mirai.utils.LoginSolver.Companion.Default
+import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
 import java.awt.Image
 import java.awt.image.BufferedImage
 import java.io.File
@@ -29,6 +31,9 @@ import kotlin.coroutines.CoroutineContext
 
 /**
  * 验证码, 设备锁解决器
+ *
+ * @see Default
+ * @see BotConfiguration.loginSolver
  */
 public abstract class LoginSolver {
     /**
@@ -61,46 +66,43 @@ public abstract class LoginSolver {
     public abstract suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String?
 
     public companion object {
-        public val Default: LoginSolver = kotlin.run {
-            if (WindowHelperJvm.isDesktopSupported) {
-                SwingSolver
-            } else {
-                DefaultLoginSolver({ readLine() ?: throw NoStandardInputForCaptchaException(null) })
-            }
+        /**
+         * 当前平台默认的 [LoginSolver]。
+         *
+         * 检测策略:
+         * 1. 检测 `android.util.Log`, 如果存在, 返回 `null`.
+         * 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 []
+         * 2. 检测 JVM 桌面环境,
+         *
+         * 在桌面 JVM, Mirai 会检测 Java Swing, 在可用时首选 [SwingSolver]. 可以通过 `System.setProperty("mirai.no-desktop", "true")` 关闭
+         * 在 Android, mirai 检测 `android.util.Log`. 然后
+         *
+         * @return [SwingSolver] 或
+         */
+        @JvmField
+        public val Default: LoginSolver? = when (WindowHelperJvm.platformKind) {
+            WindowHelperJvm.PlatformKind.ANDROID -> null
+            WindowHelperJvm.PlatformKind.SWING -> SwingSolver
+            WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
         }
-    }
-}
-
-
-/**
- * 自动选择 [SwingSolver] 或 [StandardCharImageLoginSolver]
- */
-@MiraiExperimentalApi
-public class DefaultLoginSolver(
-    public val input: suspend () -> String,
-    overrideLogger: MiraiLogger? = null
-) : LoginSolver() {
-    private val delegate: LoginSolver = StandardCharImageLoginSolver(input, overrideLogger)
-
-    override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? {
-        return delegate.onSolvePicCaptcha(bot, data)
-    }
-
-    override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? {
-        return delegate.onSolveSliderCaptcha(bot, url)
-    }
 
-    override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String? {
-        return delegate.onSolveUnsafeDeviceLoginVerify(bot, url)
+        @Suppress("unused")
+        @Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
+        public fun getDefault(): LoginSolver = Default
+            ?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
     }
 }
 
 /**
+ * CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
+ *
  * 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [overrideLogger] 输出
+ *
+ * @see createBlocking
  */
 @MiraiExperimentalApi
 public class StandardCharImageLoginSolver(
-    input: suspend () -> String,
+    input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
     /**
      * 为 `null` 时使用 [Bot.logger]
      */
@@ -166,6 +168,28 @@ public class StandardCharImageLoginSolver(
             logger.info("正在提交中...")
         }
     }
+
+    public companion object {
+        /**
+         * 创建 Java 阻塞版 [input] 的 [StandardCharImageLoginSolver]
+         *
+         * @param input 将在协程 IO 池执行, 可以有阻塞调用
+         */
+        @JvmStatic
+        public fun createBlocking(input: () -> String, output: MiraiLogger?): StandardCharImageLoginSolver {
+            return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } }, output)
+        }
+
+        /**
+         * 创建 Java 阻塞版 [input] 的 [StandardCharImageLoginSolver]
+         *
+         * @param input 将在协程 IO 池执行, 可以有阻塞调用
+         */
+        @JvmStatic
+        public fun createBlocking(input: () -> String): StandardCharImageLoginSolver {
+            return StandardCharImageLoginSolver({ withContext(Dispatchers.IO) { input() } })
+        }
+    }
 }
 
 ///////////////////////////////

+ 32 - 32
mirai-core-api/src/commonMain/kotlin/utils/SwingSolver.kt

@@ -70,39 +70,39 @@ public object SwingSolver : LoginSolver() {
 // 不会触发各种 NoDefClassError
 @Suppress("DEPRECATION")
 internal object WindowHelperJvm {
-    internal val isDesktopSupported: Boolean = kotlin.run {
-        if (System.getProperty("mirai.no-desktop") === null) {
-            kotlin.runCatching {
-                Class.forName("java.awt.Desktop")
-                Class.forName("java.awt.Toolkit")
-            }.onFailure { return@run false } // Android OS
-            kotlin.runCatching {
-                Toolkit.getDefaultToolkit()
-            }.onFailure { // AWT Error, #270
-                return@run false
-            }
-            kotlin.runCatching {
-                Desktop.isDesktopSupported().also { stat ->
-                    if (stat) {
-                        MiraiLogger.TopLevel.info(
-                            """
-                                Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
-                            """.trimIndent()
-                        )
-                        MiraiLogger.TopLevel.info(
-                            """
-                                Mirai is using desktop. Captcha will be thrown by window popup. You can add `mirai.no-desktop` to JVM properties (-Dmirai.no-desktop) to disable it.
-                            """.trimIndent()
-                        )
-                    }
-                }
-            }.getOrElse {
-                // Should not happen
-                MiraiLogger.TopLevel.warning("Exception in checking desktop support.", it)
-                false
+    enum class PlatformKind {
+        ANDROID,
+        SWING,
+        CLI
+    }
+
+    internal val platformKind: PlatformKind = kotlin.run {
+        if (kotlin.runCatching { Class.forName("android.util.Log") }.isSuccess) {
+            // Android platform
+            return@run PlatformKind.ANDROID
+        }
+        kotlin.runCatching {
+            Class.forName("java.awt.Desktop")
+            Class.forName("java.awt.Toolkit")
+            Toolkit.getDefaultToolkit()
+
+            if (Desktop.isDesktopSupported()) {
+                MiraiLogger.TopLevel.info(
+                    """
+                                    Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
+                                """.trimIndent()
+                )
+                MiraiLogger.TopLevel.info(
+                    """
+                                    Mirai is using desktop. Captcha will be thrown by window popup. You can add `mirai.no-desktop` to JVM properties (-Dmirai.no-desktop) to disable it.
+                                """.trimIndent()
+                )
+                return@run PlatformKind.SWING
+            } else {
+                return@run PlatformKind.CLI
             }
-        } else {
-            false
+        }.getOrElse {
+            return@run PlatformKind.CLI
         }
     }
 }

+ 14 - 3
mirai-core/src/commonMain/kotlin/network/QQAndroidBotNetworkHandler.kt

@@ -152,17 +152,28 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
         logger.info { "Connected to server $host:$port" }
         startPacketReceiverJobOrKill(CancellationException("relogin", cause))
 
+        fun LoginSolver?.notnull(): LoginSolver {
+            checkNotNull(this) {
+                "No LoginSolver found. Please provide by BotConfiguration.loginSolver. " +
+                        "For example use `BotFactory.newBot(...) { loginSolver = yourLoginSolver}` in Kotlin, " +
+                        "use `BotFactory.newBot(..., new BotConfiguration() {{ setLoginSolver(yourLoginSolver) }})` in Java."
+            }
+            return this
+        }
+
+        fun loginSolverNotNull() = bot.configuration.loginSolver.notnull()
+
         var response: WtLogin.Login.LoginPacketResponse = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
         mainloop@ while (true) {
             when (response) {
                 is WtLogin.Login.LoginPacketResponse.UnsafeLogin -> {
-                    bot.configuration.loginSolver.onSolveUnsafeDeviceLoginVerify(bot, response.url)
+                    loginSolverNotNull().onSolveUnsafeDeviceLoginVerify(bot, response.url)
                     response = WtLogin.Login.SubCommand9(bot.client).sendAndExpect()
                 }
 
                 is WtLogin.Login.LoginPacketResponse.Captcha -> when (response) {
                     is WtLogin.Login.LoginPacketResponse.Captcha.Picture -> {
-                        var result = bot.configuration.loginSolver.onSolvePicCaptcha(bot, response.data)
+                        var result = loginSolverNotNull().onSolvePicCaptcha(bot, response.data)
                         if (result == null || result.length != 4) {
                             //refresh captcha
                             result = "ABCD"
@@ -172,7 +183,7 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
                         continue@mainloop
                     }
                     is WtLogin.Login.LoginPacketResponse.Captcha.Slider -> {
-                        val ticket = bot.configuration.loginSolver.onSolveSliderCaptcha(bot, response.url).orEmpty()
+                        val ticket = loginSolverNotNull().onSolveSliderCaptcha(bot, response.url).orEmpty()
                         response = WtLogin.Login.SubCommand2.SubmitSliderCaptcha(bot.client, ticket).sendAndExpect()
                         continue@mainloop
                     }