From 974be88410e88fd6271065ea196b42973ed738d0 Mon Sep 17 00:00:00 2001 From: Him188 Date: Wed, 11 Jan 2023 21:23:16 +0000 Subject: [PATCH] [core] Remove SwingSolver and SeleniumLoginSolver. Always use StandardCharImageLoginSolver on desktop JVM. Close #2410 --- docs/Bots.md | 4 +- .../commonMain/kotlin/utils/LoginSolver.kt | 9 +- .../utils/SeleniumLoginSolverSupport.kt | 41 -- .../jvmMain/kotlin/utils/LoginSolver.jvm.kt | 15 +- .../src/jvmMain/kotlin/utils/SwingSolver.kt | 466 ------------------ 5 files changed, 6 insertions(+), 529 deletions(-) delete mode 100644 mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt delete mode 100644 mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt diff --git a/docs/Bots.md b/docs/Bots.md index b09cea194..2feba3049 100644 --- a/docs/Bots.md +++ b/docs/Bots.md @@ -105,8 +105,8 @@ setProtocol(MiraiProtocol.ANDROID_PAD) 在登录时可能遇到图形验证码或滑动验证码,Mirai 会使用 `LoginSolver` 解决验证码。 -- 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现,通常不需要手动提供 -- 在 Android 需要手动提供 `LoginSolver` +- 在 JVM, Mirai 提供默认的命令行实现 +- 在 Android 需要手动实现 `LoginSolver` 若要覆盖默认的 `LoginSolver` (通常不需要): ``` diff --git a/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt b/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt index 1ca99cc92..ce3371b91 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt +++ b/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 diff --git a/mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt b/mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt deleted file mode 100644 index c13fcd79e..000000000 --- a/mirai-core-api/src/jvmMain/kotlin/internal/utils/SeleniumLoginSolverSupport.kt +++ /dev/null @@ -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 - } -} diff --git a/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt b/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt index 1dbd0eb2e..44ddaa930 100644 --- a/mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt +++ b/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() } } diff --git a/mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt b/mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt deleted file mode 100644 index 7e5eb6509..000000000 --- a/mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt +++ /dev/null @@ -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 - */ - -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( - """ - - 需要滑动验证码, 完成后请输入ticket
- @see: https://github.com/project-mirai/mirai-login-solver-selenium
- @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( - "发送验证码失败.

${ - it.stackTraceToString().replace("\n", "
").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( - """ - - 需要进行短信验证码验证
- 一条短信验证码将发送到你的手机 $phoneNumberTip
- 运营商可能会收取正常短信费用
- """.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( - """ - - 需要进行账户安全认证
- 该账户有设备锁/不常用登录地点/不常用设备登录的问题
- 请在手机 QQ 打开下面链接 - 成功后请关闭该窗口 - """.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("$text") - 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> - additionInputs: Array?, - hiddenInput: Boolean = false, - topComponent: Component? = null, - parentComponent: Component? = null, - val value: JTextField = JTextField("", 15), -) { - val def = CompletableDeferred() - 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.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")) -}