Browse Source

[core] Remove SwingSolver and SeleniumLoginSolver. Always use StandardCharImageLoginSolver on desktop JVM. Close #2410

Him188 3 năm trước cách đây
mục cha
commit
974be88410

+ 2 - 2
docs/Bots.md

@@ -105,8 +105,8 @@ setProtocol(MiraiProtocol.ANDROID_PAD)
 
 在登录时可能遇到图形验证码或滑动验证码,Mirai 会使用 `LoginSolver` 解决验证码。
 
-- 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现,通常不需要手动提供
-- 在 Android 需要手动提供 `LoginSolver`
+- 在 JVM, Mirai 提供默认的命令行实现
+- 在 Android 需要手动实现 `LoginSolver`
 
 若要覆盖默认的 `LoginSolver` (通常不需要):
 ```

+ 2 - 7
mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt

@@ -123,13 +123,8 @@ public abstract class LoginSolver {
         /**
          * 当前平台默认的 [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`
+         * 在 Android 环境时, 此函数返回 `null`.
+         * 在其他 JVM 环境时, 此函数返回一个默认实现, 它通常会是 [StandardCharImageLoginSolver][net.mamoe.mirai.utils.StandardCharImageLoginSolver], 但调用方不应该依赖该属性.
          */
         @JvmField
         public val Default: LoginSolver? = PlatformLoginSolverImplementations.default

+ 0 - 41
mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt

@@ -1,41 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.internal.utils
-
-import net.mamoe.mirai.utils.LoginSolver
-import net.mamoe.mirai.utils.MiraiLogger
-
-internal val SeleniumLoginSolver: LoginSolver? by lazy {
-    try {
-        Class.forName("net.mamoe.mirai.selenium.SeleniumLoginSolver")
-            .getMethod("getInstance")
-            .invoke(null) as? LoginSolver
-    } catch (ignore: ClassNotFoundException) {
-        null
-    } catch (error: Throwable) {
-        logger.warning("Error in loading mirai-login-solver-selenium, skip", error)
-        null
-    }
-}
-
-private val logger by lazy {
-    MiraiLogger.Factory.create(LoginSolver::class)
-}
-
-// null -> 该情况为 user 确认能自己传入 ticket, 不需要 Selenium 的帮助
-// true -> SeleniumLoginSolver 支持
-// false-> 无法提供默认滑块验证解决器
-internal val isSliderCaptchaSupportKind: Boolean? by lazy {
-    if (System.getProperty("mirai.slider.captcha.supported") != null) {
-        null
-    } else {
-        SeleniumLoginSolver != null
-    }
-}

+ 2 - 13
mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

@@ -17,8 +17,6 @@ import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import net.mamoe.mirai.Bot
-import net.mamoe.mirai.internal.utils.SeleniumLoginSolver
-import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
 import net.mamoe.mirai.network.NoStandardInputForCaptchaException
 import net.mamoe.mirai.utils.StandardCharImageLoginSolver.Companion.createBlocking
 import java.awt.Image
@@ -29,18 +27,9 @@ import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
 
 internal actual object PlatformLoginSolverImplementations {
-    actual val isSliderCaptchaSupported: Boolean get() = isSliderCaptchaSupportKind ?: true
+    actual val isSliderCaptchaSupported: Boolean get() = default!!.isSliderCaptchaSupported
     actual val default: LoginSolver? by lazy {
-        when (WindowHelperJvm.platformKind) {
-            WindowHelperJvm.PlatformKind.ANDROID -> null
-            WindowHelperJvm.PlatformKind.SWING -> {
-                when (isSliderCaptchaSupportKind) {
-                    null, false -> SwingSolver
-                    true -> SeleniumLoginSolver ?: SwingSolver
-                }
-            }
-            WindowHelperJvm.PlatformKind.CLI -> StandardCharImageLoginSolver()
-        }
+        StandardCharImageLoginSolver()
     }
 }
 

+ 0 - 466
mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt

@@ -1,466 +0,0 @@
-/*
- * Copyright 2019-2022 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-@file:Suppress("unused", "MemberVisibilityCanBePrivate")
-
-package net.mamoe.mirai.utils
-
-/**
- * @author Karlatemp <[email protected]> <https://github.com/Karlatemp>
- */
-
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import net.mamoe.mirai.Bot
-import java.awt.*
-import java.awt.event.*
-import java.awt.image.BufferedImage
-import java.net.URI
-import javax.imageio.ImageIO
-import javax.swing.*
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.minutes
-
-@MiraiExperimentalApi
-public object SwingSolver : LoginSolver() {
-    override val isSliderCaptchaSupported: Boolean get() = true
-
-    public override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String {
-        val image = runBIO { ImageIO.read(data.inputStream()) }
-        return SwingLoginSolver(
-            "Mirai PicCaptcha(${bot.id})",
-            "Pic Captcha",
-            null,
-            topComponent = JLabel(ImageIcon(image)),
-        ).openAndWait()
-    }
-
-    public override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String? = coroutineScope {
-        val openWithTxCaptchaHelper = JButton("Open with TxCaptchaHelper")
-        val solver = SwingLoginSolver(
-            "Mirai SliderCaptcha(${bot.id})",
-            "ticket",
-            arrayOf(
-                "URL", JTextField(url),
-                "", openWithTxCaptchaHelper,
-            ),
-            topComponent = JLabel(
-                """
-                <html>
-                需要滑动验证码, 完成后请输入ticket <br/>
-                @see: https://github.com/project-mirai/mirai-login-solver-selenium <br/>
-                @see: https://docs.mirai.mamoe.net/mirai-login-solver-selenium/
-                """.trimIndent()
-            ),
-        )
-
-        fun JButton.doClickEvent() = launch {
-            val status = JTextField("Requesting...")
-            val txhelperSolverConfirmButton = JButton("确定")
-            val txhelperSolver = SwingLoginSolver(
-                "Mirai SliderCaptcha(${bot.id}) (TxCaptchaHelper)",
-                "",
-                arrayOf(
-                    "", status,
-                    "",
-                    JButton("Open TxHelperSolver site").onClick {
-                        openBrowserOrAlert(
-                            "https://github.com/mzdluo123/TxCaptchaHelper",
-                            "TxCaptchaHelper",
-                            "TxCaptchaHelper",
-                            getWindowForComponent(this),
-                        )
-                    },
-                    "", txhelperSolverConfirmButton,
-                ),
-                hiddenInput = true,
-                parentComponent = this@doClickEvent,
-                value = status,
-            )
-            val helper = object : TxCaptchaHelper() {
-                override fun onComplete(ticket: String) {
-                    txhelperSolver.def.complete(ticket)
-                }
-
-                override fun updateDisplay(msg: String) {
-                    status.text = msg
-                }
-            }
-            helper.start(this, url)
-            txhelperSolver.def.invokeOnCompletion { helper.dispose() }
-            solver.def.complete(txhelperSolver.openAndWait().trim())
-        }
-        openWithTxCaptchaHelper.onClick { doClickEvent() }
-        return@coroutineScope solver.openAndWait().takeIf { it.isNotEmpty() }
-    }
-
-    @Suppress("DuplicatedCode")
-    override suspend fun onSolveDeviceVerification(
-        bot: Bot,
-        requests: DeviceVerificationRequests
-    ): DeviceVerificationResult {
-        requests.sms?.let { req ->
-            solveSms(bot, req)?.let { return it }
-        }
-        requests.fallback?.let { fallback ->
-            solveFallback(bot, fallback.url)
-            return fallback.solved()
-        }
-        error("User rejected SMS login while fallback login method not available.")
-    }
-
-    @Deprecated(
-        "Please use onSolveDeviceVerification instead",
-        replaceWith = ReplaceWith("onSolveDeviceVerification(bot, url, null)"),
-        level = DeprecationLevel.WARNING
-    )
-    public override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String {
-        return solveFallback(bot, url)
-    }
-
-    private suspend fun solveSms(bot: Bot, request: DeviceVerificationRequests.SmsRequest): DeviceVerificationResult? =
-        coroutineScope {
-            val smsRequester = object {
-                var lastRequested = 0L
-
-                fun requestSms(parentComponent: Component) = launch {
-                    // oh, so shit code
-
-                    val diff = (System.currentTimeMillis() - lastRequested).milliseconds
-                    if (diff < 1.minutes) {
-                        parentComponent.createTip(
-                            """请求过于频繁, 请在 ${1.minutes - diff} 秒后再试""".trimIndent()
-                    ).openAndWait()
-                    return@launch
-                }
-
-                lastRequested = System.currentTimeMillis()
-                kotlin.runCatching {
-                    request.requestSms()
-                }.fold(
-                    onSuccess = {
-                        parentComponent.createTip(
-                            """发送验证码成功, 请注意查收. 若未收到, 可在一分钟后重试.""".trimIndent()
-                        ).openAndWait()
-                    },
-                    onFailure = {
-                        parentComponent.createTip(
-                            "<html>发送验证码失败.<br/><br/>${
-                                it.stackTraceToString().replace("\n", "<br/>").replace("\r", "")
-                            }"
-                        ).openAndWait()
-                    }
-                )
-            }
-        }
-        val title = "Mirai Device Verification (${bot.id})"
-        val phoneNumber = request.phoneNumber
-        val countryCode = request.countryCode
-        val phoneNumberTip = if (phoneNumber != null && countryCode != null)
-            """(+$countryCode) $phoneNumber"""
-        else "(无法获取到手机号码)"
-
-
-        val code = SwingLoginSolver(
-            title, "",
-            arrayOf(
-                "",
-                JButton("发送验证码").onClick {
-                    smsRequester.requestSms(this)
-                },
-                "",
-                JLabel("验证码 (输入完成后按回车):")
-            ),
-            hiddenInput = false,
-            topComponent = JLabel(
-                """
-                    <html>
-                    需要进行短信验证码验证<br>
-                    一条短信验证码将发送到你的手机 $phoneNumberTip<br>
-                    运营商可能会收取正常短信费用<br>
-                """.trimIndent()
-            )
-        ).openAndWait().trim().ifEmpty { return@coroutineScope null }
-        request.solved(code)
-    }
-
-    private fun Component.createTip(tip: String) = SwingLoginSolver(
-        "提示", "",
-        arrayOf("", JLabel()),
-        hiddenInput = true,
-        topComponent = JLabel(tip),
-        parentComponent = this,
-    )
-
-    private suspend fun solveFallback(bot: Bot, url: String): String {
-        val title = "Mirai Device Verification (${bot.id})"
-        return SwingLoginSolver(
-            title, "",
-            arrayOf(
-                "", HyperLinkLabel(url, "设备锁验证", title),
-                "URL", JTextField(url),
-            ),
-            hiddenInput = true,
-            topComponent = JLabel(
-                """
-                    <html>
-                    需要进行账户安全认证<br>
-                    该账户有设备锁/不常用登录地点/不常用设备登录的问题<br>
-                    请在<b>手机 QQ</b> 打开下面链接
-                    成功后请关闭该窗口
-                """.trimIndent()
-            )
-        ).openAndWait()
-    }
-}
-
-
-// 隔离类代码
-// 在 jvm 中, 使用 WindowHelperJvm 不会加载 SwingSolverKt
-// 不会触发各种 NoDefClassError
-internal object WindowHelperJvm {
-    enum class PlatformKind {
-        ANDROID,
-        SWING,
-        CLI
-    }
-
-    private val logger = MiraiLogger.Factory.create(this::class)
-
-    internal val platformKind: PlatformKind = kotlin.run {
-        if (kotlin.runCatching { Class.forName("android.util.Log") }.isSuccess) {
-            // Android platform
-            return@run PlatformKind.ANDROID
-        }
-        if (System.getProperty("mirai.no-desktop") != null) return@run PlatformKind.CLI
-        kotlin.runCatching {
-            Class.forName("java.awt.GraphicsEnvironment")
-            if (GraphicsEnvironment.isHeadless()) return@run PlatformKind.CLI
-
-            Class.forName("java.awt.Desktop")
-            Class.forName("java.awt.Toolkit")
-            Toolkit.getDefaultToolkit()
-
-            if (Desktop.isDesktopSupported()) {
-                logger.info(
-                    """
-                                    Mirai 正在使用桌面环境. 如遇到验证码将会弹出对话框. 可添加 JVM 属性 `mirai.no-desktop` 以关闭.
-                                """.trimIndent()
-                )
-                logger.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
-            }
-        }.onFailure { error ->
-            if (error.javaClass == ClassNotFoundException::class.java && error.cause == null) return@onFailure
-            logger.warning("Failed to initialize module `java.desktop`", error)
-        }.getOrElse {
-            return@run PlatformKind.CLI
-        }
-    }
-}
-
-
-internal val windowImage: BufferedImage? by lazy {
-    WindowHelperJvm::class.java.getResourceAsStream("project-mirai.png")?.use {
-        ImageIO.read(it)
-    }
-}
-
-internal val windowIcon: Icon? by lazy {
-    windowImage?.let(::ImageIcon)
-}
-
-/**
- * @param url 打开的链接
- * @param text 显示的提示内容
- * @param fallbackTitle 无法打开链接时的提醒窗口标题
- */
-internal class HyperLinkLabel constructor(
-    url: String,
-    text: String,
-    fallbackTitle: String
-) : JLabel() {
-    init {
-        super.setText("<html><a href='$url'>$text</a></html>")
-        addMouseListener(object : MouseAdapter() {
-
-            override fun mouseClicked(e: MouseEvent) {
-                openBrowserOrAlert(
-                    url,
-                    "Mirai 无法直接打开浏览器, 请手动复制以下 URL 打开",
-                    fallbackTitle,
-                    this@HyperLinkLabel
-                )
-            }
-        })
-    }
-}
-
-internal class SwingLoginSolver(
-    title: String?,
-    inputType: String?,
-    // Array<[inlined] Pair<String, Component>>
-    additionInputs: Array<Any>?,
-    hiddenInput: Boolean = false,
-    topComponent: Component? = null,
-    parentComponent: Component? = null,
-    val value: JTextField = JTextField("", 15),
-) {
-    val def = CompletableDeferred<String>()
-    val frame: Window = if (parentComponent == null) {
-        JFrame(title)
-    } else {
-        JDialog(JOptionPane.getFrameForComponent(parentComponent), title, true)
-    }
-
-    init {
-        if (frame is JFrame) {
-            frame.iconImage = windowImage
-            frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
-        }
-        frame.minimumSize = Dimension(228, 62)
-        frame.layout = BorderLayout(5, 5)
-        kotlin.run {
-            val contentPane = JPanel()
-            // contentPane.background = Color.cyan
-            frame.add(contentPane, BorderLayout.PAGE_END)
-            val label = JLabel(inputType)
-            label.labelFor = value
-            val layout = GroupLayout(contentPane)
-            contentPane.layout = layout
-            layout.autoCreateGaps = true
-            layout.autoCreateContainerGaps = true
-            kotlin.run {
-                val lining = layout.createSequentialGroup()
-                val left = layout.createParallelGroup()
-                val right = layout.createParallelGroup()
-                if (topComponent != null) lining.addComponent(topComponent)
-                if (additionInputs != null) {
-                    var i = 0
-                    while (i < additionInputs.size) {
-                        val left0 = JLabel(additionInputs[i].toString())
-                        val right0 = additionInputs[i + 1] as Component
-                        left0.labelFor = right0
-                        left.addComponent(left0)
-                        right.addComponent(right0)
-                        lining.addGroup(
-                            layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
-                                .addComponent(left0)
-                                .addComponent(right0)
-                        )
-                        i += 2
-                    }
-                }
-                if (!hiddenInput) {
-                    left.addComponent(label)
-                    right.addComponent(value)
-                }
-                layout.setHorizontalGroup(
-                    layout.createParallelGroup()
-                        .also { group ->
-                            if (topComponent != null) {
-                                group.addComponent(topComponent)
-                            }
-                        }
-                        .addGroup(
-                            layout.createSequentialGroup()
-                                .addGroup(left)
-                                .addGroup(right)
-                        )
-                )
-                if (hiddenInput) {
-                    layout.setVerticalGroup(lining)
-                } else {
-                    layout.setVerticalGroup(
-                        lining.addGroup(
-                            layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
-                                .addComponent(label)
-                                .addComponent(value)
-                        )
-                    )
-                }
-            }
-        }
-        value.addKeyListener(object : KeyListener {
-            override fun keyTyped(e: KeyEvent?) {
-            }
-
-            override fun keyPressed(e: KeyEvent?) {
-                when (e!!.keyCode) {
-                    27, 10 -> {
-                        def.complete(value.text)
-                    }
-                }
-            }
-
-            override fun keyReleased(e: KeyEvent?) {
-            }
-        })
-        frame.addWindowListener(object : WindowAdapter() {
-            override fun windowClosing(e: WindowEvent?) {
-                def.complete(value.text)
-            }
-        })
-    }
-
-    suspend fun openAndWait(): String {
-        frame.pack()
-        frame.setLocationRelativeTo(null)
-        runBIO {
-            def.invokeOnCompletion {
-                SwingUtilities.invokeLater {
-                    frame.dispose()
-                }
-            }
-            frame.isVisible = true
-        }
-        return def.await()
-    }
-}
-
-private fun openBrowserOrAlert(
-    url: String,
-    msg: String,
-    title: String,
-    component: Component? = null,
-) {
-    // Try to open browser safely. #694
-    try {
-        Desktop.getDesktop().browse(URI(url))
-    } catch (ex: Exception) {
-        JOptionPane.showInputDialog(
-            component,
-            msg,
-            title,
-            JOptionPane.WARNING_MESSAGE,
-            windowIcon,
-            null,
-            url
-        )
-    }
-}
-
-private fun <T : Component> T.onClick(onclick: T.(MouseEvent) -> Unit): T = apply {
-    addMouseListener(object : MouseAdapter() {
-        override fun mouseClicked(e: MouseEvent) {
-            onclick(this@onClick, e)
-        }
-    })
-}
-
-private tailrec fun getWindowForComponent(component: Component): Window {
-    if (component is Window) return component
-    return getWindowForComponent(component.parent ?: error("Component not attached"))
-}