mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-04 13:19:15 +08:00
[core] Remove SwingSolver and SeleniumLoginSolver. Always use StandardCharImageLoginSolver on desktop JVM. Close #2410
This commit is contained in:
parent
3cd74ff45b
commit
974be88410
@ -105,8 +105,8 @@ setProtocol(MiraiProtocol.ANDROID_PAD)
|
||||
|
||||
在登录时可能遇到图形验证码或滑动验证码,Mirai 会使用 `LoginSolver` 解决验证码。
|
||||
|
||||
- 在 JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现,通常不需要手动提供
|
||||
- 在 Android 需要手动提供 `LoginSolver`
|
||||
- 在 JVM, Mirai 提供默认的命令行实现
|
||||
- 在 Android 需要手动实现 `LoginSolver`
|
||||
|
||||
若要覆盖默认的 `LoginSolver` (通常不需要):
|
||||
```
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 <karlatemp@vip.qq.com> <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"))
|
||||
}
|
Loading…
Reference in New Issue
Block a user