mirror of
https://github.com/mamoe/mirai.git
synced 2025-04-25 04:50:26 +08:00
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)
This commit is contained in:
parent
56e7d4de3d
commit
e3b553b4de
@ -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() }
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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() } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
|
@ -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
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}.getOrElse {
|
||||
return@run PlatformKind.CLI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user