فهرست منبع

Move StandardCharImageLoginSolver from common to jvm

Him188 5 سال پیش
والد
کامیت
ba61194fa4
2فایلهای تغییر یافته به همراه201 افزوده شده و 200 حذف شده
  1. 4 199
      mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt
  2. 197 1
      mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

+ 4 - 199
mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt

@@ -9,26 +9,11 @@
 
 package net.mamoe.mirai.utils
 
-import kotlinx.coroutines.CoroutineName
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.io.*
-import kotlinx.coroutines.io.jvm.nio.copyTo
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-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.DeviceInfo.Companion.loadAsDeviceInfo
 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
-import java.io.RandomAccessFile
-import javax.imageio.ImageIO
-import kotlin.coroutines.CoroutineContext
 
 /**
  * 验证码, 设备锁解决器
@@ -81,11 +66,11 @@ public expect abstract class LoginSolver() {
          *
          * 检测策略:
          * 1. 若是 `mirai-core-api-android` 或 `android.util.Log` 存在, 返回 `null`.
-         * 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 [StandardCharImageLoginSolver]
+         * 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 `StandardCharImageLoginSolver`
          * 3. 检测 JVM 桌面环境, 若支持, 返回 `SwingSolver`
-         * 4. 返回 [StandardCharImageLoginSolver]
+         * 4. 返回 `StandardCharImageLoginSolver`
          *
-         * @return `SwingSolver` 或 [StandardCharImageLoginSolver] 或 `null`
+         * @return `SwingSolver` 或 `StandardCharImageLoginSolver` 或 `null`
          */
         @JvmField
         public val Default: LoginSolver?
@@ -97,188 +82,8 @@ public expect abstract class LoginSolver() {
 
 }
 
-/**
- * CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
- *
- * 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
- *
- * @see createBlocking
- */
-public class StandardCharImageLoginSolver @JvmOverloads constructor(
-    input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
-    /**
-     * 为 `null` 时使用 [Bot.logger]
-     */
-    private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
-) : LoginSolver() {
-    public constructor(
-        input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
-        overrideLogger: MiraiLogger?
-    ) : this(input, { overrideLogger ?: it.logger })
-
-    private val input: suspend () -> String = suspend {
-        withContext(Dispatchers.IO) { input() }
-    }
-
-    override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
-        val logger = loggerSupplier(bot)
-        @Suppress("BlockingMethodInNonBlockingContext")
-        withContext(Dispatchers.IO) {
-            val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
-            tempFile.createNewFile()
-            logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
-            logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
-            try {
-                tempFile.writeChannel().apply { writeFully(data); close() }
-                logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
-                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
-            } catch (e: Exception) {
-                logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
-                logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
-            }
-
-            tempFile.inputStream().use { stream ->
-                try {
-                    val img = ImageIO.read(stream)
-                    if (img == null) {
-                        logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
-                        logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
-                    } else {
-                        logger.info { "[PicCaptcha] \n" + img.createCharImg() }
-                    }
-                } catch (throwable: Throwable) {
-                    logger.warning("[PicCaptcha] 创建字符图片时出错. 请查看文件.", throwable)
-                    logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
-                }
-            }
-        }
-        logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
-        logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
-        return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
-            logger.info { "[PicCaptcha] 正在提交 $it..." }
-            logger.info { "[PicCaptcha] Submitting $it..." }
-        }
-    }
-
-    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] @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)
-        return input().also {
-            logger.info { "[SliderCaptcha] 正在提交中..." }
-            logger.info { "[SliderCaptcha] Submitting..." }
-        }
-    }
-
-    override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
-        val logger = loggerSupplier(bot)
-        logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
-        logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
-        return input().also {
-            logger.info { "[UnsafeLogin] 正在提交中..." }
-            logger.info { "[UnsafeLogin] Submitting..." }
-        }
-    }
-
-    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() } })
-        }
-    }
-}
-
-///////////////////////////////
-//////////////// internal
-///////////////////////////////
-
 internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
     return {
         file().loadAsDeviceInfo(json)
     }
-}
-
-// Copied from Ktor CIO
-private fun File.writeChannel(
-    coroutineContext: CoroutineContext = Dispatchers.IO
-): ByteWriteChannel = GlobalScope.reader(CoroutineName("file-writer") + coroutineContext, autoFlush = true) {
-    @Suppress("BlockingMethodInNonBlockingContext")
-    RandomAccessFile(this@writeChannel, "rw").use { file ->
-        val copied = channel.copyTo(file.channel)
-        file.setLength(copied) // truncate tail that could remain from the previously written data
-    }
-}.channel
-
-private val loginSolverLock = Mutex()
-
-/**
- * @author NaturalHG
- */
-private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String {
-    val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt()
-    val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH)
-    val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB)
-    val g2d = image.createGraphics()
-    g2d.drawImage(tmp, 0, 0, null)
-    fun gray(rgb: Int): Int {
-        val r = rgb and 0xff0000 shr 16
-        val g = rgb and 0x00ff00 shr 8
-        val b = rgb and 0x0000ff
-        return (r * 30 + g * 59 + b * 11 + 50) / 100
-    }
-
-    fun grayCompare(g1: Int, g2: Int): Boolean =
-        kotlin.math.min(g1, g2).toDouble() / kotlin.math.max(g1, g2) >= ignoreRate
-
-    val background = gray(image.getRGB(0, 0))
-
-    return buildString(capacity = height) {
-
-        val lines = mutableListOf<StringBuilder>()
-
-        var minXPos = outputWidth
-        var maxXPos = 0
-
-        for (y in 0 until image.height) {
-            val builderLine = StringBuilder()
-            for (x in 0 until image.width) {
-                val gray = gray(image.getRGB(x, y))
-                if (grayCompare(gray, background)) {
-                    builderLine.append(" ")
-                } else {
-                    builderLine.append("#")
-                    if (x < minXPos) {
-                        minXPos = x
-                    }
-                    if (x > maxXPos) {
-                        maxXPos = x
-                    }
-                }
-            }
-            if (builderLine.toString().isBlank()) {
-                continue
-            }
-            lines.add(builderLine)
-        }
-        for (line in lines) {
-            append(line.substring(minXPos, maxXPos)).append("\n")
-        }
-    }
-}
+}

+ 197 - 1
mirai-core-api/src/jvmMain/kotlin/utils/LoginSolver.jvm.kt

@@ -10,11 +10,30 @@
 
 package net.mamoe.mirai.utils
 
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.io.ByteWriteChannel
+import kotlinx.coroutines.io.close
+import kotlinx.coroutines.io.jvm.nio.copyTo
+import kotlinx.coroutines.io.reader
+import kotlinx.coroutines.io.writeFully
+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.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
+import java.io.RandomAccessFile
+import javax.imageio.ImageIO
+import kotlin.coroutines.CoroutineContext
 
 
 /**
@@ -93,4 +112,181 @@ public actual abstract class LoginSolver public actual constructor() {
             ?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
     }
 
-}
+}
+
+
+/**
+ * CLI 环境 [LoginSolver]. 将验证码图片转为字符画并通过 `output` 输出, [input] 获取用户输入.
+ *
+ * 使用字符图片展示验证码, 使用 [input] 获取输入, 使用 [loggerSupplier] 输出
+ *
+ * @see createBlocking
+ */
+public class StandardCharImageLoginSolver @JvmOverloads constructor(
+    input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
+    /**
+     * 为 `null` 时使用 [Bot.logger]
+     */
+    private val loggerSupplier: (bot: Bot) -> MiraiLogger = { it.logger }
+) : LoginSolver() {
+    public constructor(
+        input: suspend () -> String = { readLine() ?: throw NoStandardInputForCaptchaException() },
+        overrideLogger: MiraiLogger?
+    ) : this(input, { overrideLogger ?: it.logger })
+
+    private val input: suspend () -> String = suspend {
+        withContext(Dispatchers.IO) { input() }
+    }
+
+    override suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String? = loginSolverLock.withLock {
+        val logger = loggerSupplier(bot)
+        @Suppress("BlockingMethodInNonBlockingContext")
+        (withContext(Dispatchers.IO) {
+            val tempFile: File = File.createTempFile("tmp", ".png").apply { deleteOnExit() }
+            tempFile.createNewFile()
+            logger.info { "[PicCaptcha] 需要图片验证码登录, 验证码为 4 字母" }
+            logger.info { "[PicCaptcha] Picture captcha required. Captcha consists of 4 letters." }
+            try {
+                tempFile.writeChannel().apply { writeFully(data); close() }
+                logger.info { "[PicCaptcha] 将会显示字符图片. 若看不清字符图片, 请查看文件 ${tempFile.absolutePath}" }
+                logger.info { "[PicCaptcha] Displaying char-image. If not clear, view file ${tempFile.absolutePath}" }
+            } catch (e: Exception) {
+                logger.warning("[PicCaptcha] 无法写出验证码文件, 请尝试查看以上字符图片", e)
+                logger.warning("[PicCaptcha] Failed to export captcha image. Please see the char-image.", e)
+            }
+
+            tempFile.inputStream().use { stream ->
+                try {
+                    val img = ImageIO.read(stream)
+                    if (img == null) {
+                        logger.warning { "[PicCaptcha] 无法创建字符图片. 请查看文件" }
+                        logger.warning { "[PicCaptcha] Failed to create char-image. Please see the file." }
+                    } else {
+                        logger.info { "[PicCaptcha] \n" + img.createCharImg() }
+                    }
+                } catch (throwable: Throwable) {
+                    logger.warning("[PicCaptcha] 创建字符图片时出错. 请查看文件.", throwable)
+                    logger.warning("[PicCaptcha] Failed to create char-image. Please see the file.", throwable)
+                }
+            }
+        })
+        logger.info { "[PicCaptcha] 请输入 4 位字母验证码. 若要更换验证码, 请直接回车" }
+        logger.info { "[PicCaptcha] Please type 4-letter captcha. Press Enter directly to refresh." }
+        return input().takeUnless { it.isEmpty() || it.length != 4 }.also {
+            logger.info { "[PicCaptcha] 正在提交 $it..." }
+            logger.info { "[PicCaptcha] Submitting $it..." }
+        }
+    }
+
+    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] @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)
+        return input().also {
+            logger.info { "[SliderCaptcha] 正在提交中..." }
+            logger.info { "[SliderCaptcha] Submitting..." }
+        }
+    }
+
+    override suspend fun onSolveUnsafeDeviceLoginVerify(bot: Bot, url: String): String = loginSolverLock.withLock {
+        val logger = loggerSupplier(bot)
+        logger.info { "[UnsafeLogin] 当前登录环境不安全,服务器要求账户认证。请在 QQ 浏览器打开 $url 并完成验证后输入任意字符。" }
+        logger.info { "[UnsafeLogin] Account verification required by the server. Please open $url in QQ browser and complete challenge, then type anything here to submit." }
+        return input().also {
+            logger.info { "[UnsafeLogin] 正在提交中..." }
+            logger.info { "[UnsafeLogin] Submitting..." }
+        }
+    }
+
+    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() } })
+        }
+    }
+}
+
+// Copied from Ktor CIO
+private fun File.writeChannel(
+    coroutineContext: CoroutineContext = Dispatchers.IO
+): ByteWriteChannel = GlobalScope.reader(CoroutineName("file-writer") + coroutineContext, autoFlush = true) {
+    @Suppress("BlockingMethodInNonBlockingContext")
+    RandomAccessFile(this@writeChannel, "rw").use { file ->
+        val copied = channel.copyTo(file.channel)
+        file.setLength(copied) // truncate tail that could remain from the previously written data
+    }
+}.channel
+
+private val loginSolverLock = Mutex()
+
+/**
+ * @author NaturalHG
+ */
+private fun BufferedImage.createCharImg(outputWidth: Int = 100, ignoreRate: Double = 0.95): String {
+    val newHeight = (this.height * (outputWidth.toDouble() / this.width)).toInt()
+    val tmp = this.getScaledInstance(outputWidth, newHeight, Image.SCALE_SMOOTH)
+    val image = BufferedImage(outputWidth, newHeight, BufferedImage.TYPE_INT_ARGB)
+    val g2d = image.createGraphics()
+    g2d.drawImage(tmp, 0, 0, null)
+    fun gray(rgb: Int): Int {
+        val r = rgb and 0xff0000 shr 16
+        val g = rgb and 0x00ff00 shr 8
+        val b = rgb and 0x0000ff
+        return (r * 30 + g * 59 + b * 11 + 50) / 100
+    }
+
+    fun grayCompare(g1: Int, g2: Int): Boolean =
+        kotlin.math.min(g1, g2).toDouble() / kotlin.math.max(g1, g2) >= ignoreRate
+
+    val background = gray(image.getRGB(0, 0))
+
+    return buildString(capacity = height) {
+
+        val lines = mutableListOf<StringBuilder>()
+
+        var minXPos = outputWidth
+        var maxXPos = 0
+
+        for (y in 0 until image.height) {
+            val builderLine = StringBuilder()
+            for (x in 0 until image.width) {
+                val gray = gray(image.getRGB(x, y))
+                if (grayCompare(gray, background)) {
+                    builderLine.append(" ")
+                } else {
+                    builderLine.append("#")
+                    if (x < minXPos) {
+                        minXPos = x
+                    }
+                    if (x > maxXPos) {
+                        maxXPos = x
+                    }
+                }
+            }
+            if (builderLine.toString().isBlank()) {
+                continue
+            }
+            lines.add(builderLine)
+        }
+        for (line in lines) {
+            append(line.substring(minXPos, maxXPos)).append("\n")
+        }
+    }
+}