1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-25 04:50:26 +08:00

Improve LoginSolver, close :

- 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:
Him188 2020-12-20 09:19:22 +08:00
parent 56e7d4de3d
commit e3b553b4de
6 changed files with 118 additions and 69 deletions
build.gradle.kts
mirai-core-api/src/commonMain/kotlin
mirai-core/src/commonMain/kotlin/network

View File

@ -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() }
}

View File

@ -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")
/**

View File

@ -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)

View File

@ -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() } })
}
}
}
///////////////////////////////

View File

@ -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
}
}
}

View File

@ -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
}