mirror of
https://github.com/mamoe/mirai.git
synced 2025-02-08 00:57:01 +08:00
Redesign LoginSolver (#1285)
* Redesign SwingSolver * TxCaptchaHelper support * Simplify TxCaptchaHelper
This commit is contained in:
parent
4fd1b25838
commit
c89d31cef6
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2019-2021 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/master/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.Mirai
|
||||
|
||||
internal abstract class TxCaptchaHelper {
|
||||
private val newClient: Boolean
|
||||
val client: HttpClient
|
||||
|
||||
init {
|
||||
var newClient = false
|
||||
client = try {
|
||||
Mirai.Http
|
||||
} catch (ignore: Throwable) {
|
||||
newClient = true
|
||||
HttpClient()
|
||||
}
|
||||
this.newClient = newClient
|
||||
}
|
||||
|
||||
internal var latestDisplay = "Sending request..."
|
||||
|
||||
abstract fun onComplete(ticket: String)
|
||||
abstract fun updateDisplay(msg: String)
|
||||
|
||||
fun start(scope: CoroutineScope, url: String) {
|
||||
val url0 = url.replace("ssl.captcha.qq.com", "txhelper.glitch.me")
|
||||
val queue = scope.launch {
|
||||
updateDisplay(latestDisplay)
|
||||
while (isActive) {
|
||||
try {
|
||||
val response: String = client.get(url0)
|
||||
if (response.startsWith("请在")) {
|
||||
if (response != latestDisplay) {
|
||||
latestDisplay = response
|
||||
updateDisplay(response)
|
||||
}
|
||||
} else {
|
||||
onComplete(response)
|
||||
return@launch
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
updateDisplay(e.toString().also { latestDisplay = it })
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
if (newClient) {
|
||||
queue.invokeOnCompletion { client.close() }
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.io.ByteWriteChannel
|
||||
import kotlinx.coroutines.io.close
|
||||
@ -19,6 +20,7 @@ import kotlinx.coroutines.io.writeFully
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.Mirai
|
||||
import net.mamoe.mirai.internal.utils.SeleniumLoginSolver
|
||||
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
|
||||
import net.mamoe.mirai.network.LoginFailedException
|
||||
@ -31,6 +33,8 @@ import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import javax.imageio.ImageIO
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
/**
|
||||
@ -177,11 +181,35 @@ public class StandardCharImageLoginSolver @JvmOverloads constructor(
|
||||
|
||||
override suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String = loginSolverLock.withLock {
|
||||
val logger = loggerSupplier(bot)
|
||||
logger.info { "[SliderCaptcha] 需要滑动验证码, 请在浏览器中打开以下链接并完成验证码, 完成后请输入提示 ticket." }
|
||||
logger.info { "[SliderCaptcha] Slider captcha required, please open the following link in any browser and solve the captcha. Type ticket here after completion." }
|
||||
logger.info { "[SliderCaptcha] 需要滑动验证码, 请按照以下链接的步骤完成滑动验证码, 然后输入获取到的 ticket" }
|
||||
logger.info { "[SliderCaptcha] Slider captcha required. Please solve the captcha with following link. Type ticket here after completion." }
|
||||
logger.info { "[SliderCaptcha] @see https://github.com/project-mirai/mirai-login-solver-selenium#%E4%B8%8B%E8%BD%BD-chrome-%E6%89%A9%E5%B1%95%E6%8F%92%E4%BB%B6" }
|
||||
logger.info(url)
|
||||
logger.info { "[SliderCaptcha] 或者输入 TxCaptchaHelper 来使用 TxCaptchaHelper 完成滑动验证码" }
|
||||
logger.info { "[SliderCaptcha] Or type `TxCaptchaHelper` to resolve slider captcha with TxCaptchaHelper.apk" }
|
||||
logger.info { "[SliderCaptcha] Captcha link: $url" }
|
||||
|
||||
suspend fun runTxCaptchaHelper(): String {
|
||||
logger.info { "[SliderCaptcha] @see https://github.com/mzdluo123/TxCaptchaHelper" }
|
||||
return coroutineScope {
|
||||
suspendCoroutine { coroutine ->
|
||||
val helper = object : TxCaptchaHelper() {
|
||||
override fun onComplete(ticket: String) {
|
||||
coroutine.resume(ticket)
|
||||
}
|
||||
|
||||
override fun updateDisplay(msg: String) {
|
||||
logger.info(msg)
|
||||
}
|
||||
}
|
||||
helper.start(this, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return input().also {
|
||||
if (it == "TxCaptchaHelper" || it == "`TxCaptchaHelper`") {
|
||||
return runTxCaptchaHelper()
|
||||
}
|
||||
logger.info { "[SliderCaptcha] 正在提交中..." }
|
||||
logger.info { "[SliderCaptcha] Submitting..." }
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
*
|
||||
* https://github.com/mamoe/mirai/blob/master/LICENSE
|
||||
*/
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
@ -13,13 +14,9 @@ package net.mamoe.mirai.utils
|
||||
* @author Karlatemp <karlatemp@vip.qq.com> <https://github.com/Karlatemp>
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.utils.*
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Desktop
|
||||
import java.awt.Dimension
|
||||
import java.awt.Toolkit
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
import java.awt.image.BufferedImage
|
||||
import java.net.URI
|
||||
@ -29,29 +26,82 @@ import javax.swing.*
|
||||
@MiraiExperimentalApi
|
||||
public object SwingSolver : LoginSolver() {
|
||||
public override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String {
|
||||
return openWindow("Mirai PicCaptcha(${bot.id})") {
|
||||
val image = ImageIO.read(data.inputStream())
|
||||
JLabel(ImageIcon(image)).append()
|
||||
}
|
||||
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? {
|
||||
return openWindow("Mirai SliderCaptcha(${bot.id})") {
|
||||
JLabel(
|
||||
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
|
||||
""".trimIndent()
|
||||
).append()
|
||||
JTextField(url).last()
|
||||
}.takeIf { it.isNotEmpty() }
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
solver.def.complete(txhelperSolver.openAndWait().trim())
|
||||
}
|
||||
openWithTxCaptchaHelper.onClick { doClickEvent() }
|
||||
return@coroutineScope solver.openAndWait().takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
public override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String {
|
||||
val title = "Mirai UnsafeDeviceLoginVerify(${bot.id})"
|
||||
return openWindow(title) {
|
||||
JLabel(
|
||||
return SwingLoginSolver(
|
||||
title, "",
|
||||
arrayOf(
|
||||
"", HyperLinkLabel(url, "设备锁验证", title),
|
||||
"URL", JTextField(url),
|
||||
),
|
||||
hiddenInput = true,
|
||||
topComponent = JLabel(
|
||||
"""
|
||||
<html>
|
||||
需要进行账户安全认证<br>
|
||||
@ -59,9 +109,8 @@ public object SwingSolver : LoginSolver() {
|
||||
完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次<br>
|
||||
成功后请关闭该窗口
|
||||
""".trimIndent()
|
||||
).append()
|
||||
HyperLinkLabel(url, "设备锁验证", title).last()
|
||||
}
|
||||
)
|
||||
).openAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,22 +158,6 @@ internal object WindowHelperJvm {
|
||||
}
|
||||
}
|
||||
|
||||
internal class WindowInitializer(private val initializer: WindowInitializer.(JFrame) -> Unit) {
|
||||
private lateinit var frame0: JFrame
|
||||
val frame: JFrame get() = frame0
|
||||
fun java.awt.Component.append() {
|
||||
frame.add(this, BorderLayout.NORTH)
|
||||
}
|
||||
|
||||
fun java.awt.Component.last() {
|
||||
frame.add(this)
|
||||
}
|
||||
|
||||
internal fun init(frame: JFrame) {
|
||||
this.frame0 = frame
|
||||
initializer(frame)
|
||||
}
|
||||
}
|
||||
|
||||
internal val windowImage: BufferedImage? by lazy {
|
||||
WindowHelperJvm::class.java.getResourceAsStream("project-mirai.png")?.use {
|
||||
@ -136,53 +169,6 @@ internal val windowIcon: Icon? by lazy {
|
||||
windowImage?.let(::ImageIcon)
|
||||
}
|
||||
|
||||
internal suspend fun openWindow(title: String = "", initializer: WindowInitializer.(JFrame) -> Unit = {}): String {
|
||||
return openWindow(title, WindowInitializer(initializer))
|
||||
}
|
||||
|
||||
internal suspend fun openWindow(title: String = "", initializer: WindowInitializer = WindowInitializer {}): String {
|
||||
val frame = JFrame()
|
||||
frame.iconImage = windowImage
|
||||
frame.minimumSize = Dimension(228, 62) // From Windows 10
|
||||
val value = JTextField()
|
||||
val def = CompletableDeferred<String>()
|
||||
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.layout = BorderLayout(10, 5)
|
||||
frame.add(value, BorderLayout.SOUTH)
|
||||
initializer.init(frame)
|
||||
|
||||
frame.pack()
|
||||
frame.defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
|
||||
frame.addWindowListener(object : WindowAdapter() {
|
||||
override fun windowClosing(e: WindowEvent?) {
|
||||
def.complete(value.text)
|
||||
}
|
||||
})
|
||||
frame.setLocationRelativeTo(null)
|
||||
frame.title = title
|
||||
frame.isVisible = true
|
||||
|
||||
return def.await().trim().also {
|
||||
SwingUtilities.invokeLater {
|
||||
frame.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param url 打开的链接
|
||||
* @param text 显示的提示内容
|
||||
@ -198,21 +184,170 @@ internal class HyperLinkLabel constructor(
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
// Try to open browser safely. #694
|
||||
try {
|
||||
Desktop.getDesktop().browse(URI(url))
|
||||
} catch (ex: Exception) {
|
||||
JOptionPane.showInputDialog(
|
||||
this@HyperLinkLabel,
|
||||
"Mirai 无法直接打开浏览器, 请手动复制以下 URL 打开",
|
||||
fallbackTitle,
|
||||
JOptionPane.WARNING_MESSAGE,
|
||||
windowIcon,
|
||||
null,
|
||||
url
|
||||
)
|
||||
}
|
||||
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