diff --git a/mirai-console/backend/integration-test/src/IntegrationTestBootstrap.kt b/mirai-console/backend/integration-test/src/IntegrationTestBootstrap.kt index b86a212c09..cbdaca7bbc 100644 --- a/mirai-console/backend/integration-test/src/IntegrationTestBootstrap.kt +++ b/mirai-console/backend/integration-test/src/IntegrationTestBootstrap.kt @@ -54,6 +54,7 @@ internal fun main() { error("Don't launch IntegrationTestBootstrap directly. See /test/MiraiConsoleIntegrationTestBootstrap.kt") } } + System.setProperty("mirai.console.skip-end-user-readme", "") // @context: env.testunit = true // @context: env.inJUnitProcess = false // @context: env.exitProcessSafety = true diff --git a/mirai-console/backend/mirai-console/build.gradle.kts b/mirai-console/backend/mirai-console/build.gradle.kts index 6598fcbeb7..ef5813ffa3 100644 --- a/mirai-console/backend/mirai-console/build.gradle.kts +++ b/mirai-console/backend/mirai-console/build.gradle.kts @@ -103,6 +103,10 @@ tasks { } } +tasks.withType { + this.jvmArgs("-Dmirai.console.skip-end-user-readme") +} + tasks.getByName("compileKotlin").dependsOn( DependencyDumper.registerDumpTaskKtSrc( project, diff --git a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api index c4f2b7e317..f91dec8dbd 100644 --- a/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api +++ b/mirai-console/backend/mirai-console/compatibility-validation/jvm/api/jvm.api @@ -1287,6 +1287,29 @@ public final class net/mamoe/mirai/console/data/java/JavaAutoSavePluginData$Comp public final fun createKType (Ljava/lang/Class;[Lkotlin/reflect/KType;)Lkotlin/reflect/KType; } +public final class net/mamoe/mirai/console/enduserreadme/EndUserReadme { + public static final field Companion Lnet/mamoe/mirai/console/enduserreadme/EndUserReadme$Companion; + public static final field DELAY Ljava/lang/String; + public static final field PAUSE Ljava/lang/String; + public fun ()V + public final fun put (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public final fun putAll (Ljava/lang/String;)V +} + +public final class net/mamoe/mirai/console/enduserreadme/EndUserReadme$Companion { +} + +public final class net/mamoe/mirai/console/enduserreadme/EndUserReadme$Render { + public fun ()V + public final fun delay ()V + public final fun delay (I)V + public final fun msg (Ljava/lang/String;)V + public final fun pause ()V + public final fun plusAssign (Ljava/lang/String;)V + public final fun render ()Ljava/lang/String; + public final fun unaryPlus (Ljava/lang/String;)V +} + public abstract class net/mamoe/mirai/console/events/AutoLoginEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/console/events/ConsoleEvent, net/mamoe/mirai/event/events/BotEvent { } @@ -1302,6 +1325,10 @@ public final class net/mamoe/mirai/console/events/AutoLoginEvent$Success : net/m public abstract interface class net/mamoe/mirai/console/events/ConsoleEvent : net/mamoe/mirai/event/Event { } +public final class net/mamoe/mirai/console/events/EndUserReadmeInitializeEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/console/events/ConsoleEvent { + public final fun getReadme ()Lnet/mamoe/mirai/console/enduserreadme/EndUserReadme; +} + public final class net/mamoe/mirai/console/events/StartupEvent : net/mamoe/mirai/event/AbstractEvent, net/mamoe/mirai/console/events/ConsoleEvent { public final fun getTimestamp ()J } diff --git a/mirai-console/backend/mirai-console/resources/net/mamoe/mirai/console/internal/enduserreadme/readme.txt b/mirai-console/backend/mirai-console/resources/net/mamoe/mirai/console/internal/enduserreadme/readme.txt new file mode 100644 index 0000000000..e2b61eb8d6 --- /dev/null +++ b/mirai-console/backend/mirai-console/resources/net/mamoe/mirai/console/internal/enduserreadme/readme.txt @@ -0,0 +1,129 @@ +::mirai-console.greeting + +欢迎使用 mirai-console。 +在您正式开始使用 mirai-console 前,您需要完整阅读此用户须知。 + +此用户须知包含 mirai-console 本体及其所安装的插件的用户须知。 +当相关的最终用户须知更新时,mirai-console 只会显示已更新部分,而不会重新完整显示整个用户须知。 + +::mirai-console.usage + +在使用 mirai-console 前,您需要完整阅读用户手册。 +2 +用户手册地址: + GitHub: https://github.com/mamoe/mirai/blob/dev/docs/UserManual.md + VuePress: https://docs.mirai.mamoe.net/UserManual.html +3 +当您遇到问题前,请先查阅 +2 + 常见问题参考: https://docs.mirai.mamoe.net/Questions.html +1 + mirai 历史问题提问: https://github.com/mamoe/mirai/issues?q=is%3Aissue +3 + +如果您使用的 mirai-console 来自一个单独整合包,您需要参考该整合包内的 `readme` 文件 + +::mirai-console.issuing + +在使用 mirai-console 的过程中,您可能会遇到各种问题。 +在您向他人咨询前,您需要做好以下准备。 +2 +无论是 +2 +`- 在 mirai 主仓库发起 issue +1 +`- 在 mirai 论坛发起帖子 +1 +`- 在群聊向他人咨询 +1 +`- 在私聊向他人咨询 +1 +`- 或者更多 +1 +您都需要做好以下准备。 +1 +这不仅能让您更快解决问题,也是对被询问者的尊重。 +1 + +1. 说明您正在使用的版本 +2 +版本号是确定问题的关键信息, +1 +mirai-console 的版本号会在 mirai-console 运行时就打印至控制台。 +其他组件版本可以通过执行 /status 命令获取 + +3 +2. 携带报错信息 / 携带日志 +3 +报错信息是分析问题的关键,没有日志相当于闭眼开车。 +3 +当您咨询时,一定要携带当时的日志 +3 +「没有日志我能做的事只有帮你算一卦」 +3 + +标准的咨询模板参考: +https://github.com/mamoe/mirai/issues/new?template=bug.yml + +::mirai-core.EncryptService.alert + +Reference: https://github.com/mamoe/mirai/releases/tag/v2.15.0 + +关于包数据加密 / 签名 (Internal)(#2716) +2 +mirai 不会内置任何第三方 签名/加密 服务,而是提供 SPI 让用户自行实现。 +2 +mirai 已经提供了外部 EncryptService SPI 供用户对接。如果您没有能力自行对接,您可以考虑到论坛寻找社区对接。 +2 +在使用社区服务前,您需要了解并理解以下内容 +2 + + +1. 确认服务来源 +2 + 当您安装此服务后,所有的信息都会经过此消息服务。 + 2 + 这其中包括 + Bot 的登录请求(包含密码,登录凭证等) + 2 + Bot 发出去的所有信息 + 2 + 更多..... +2 + +2. 保护好网络,建立通讯防火墙 +2 +部分服务通讯链路是无加密的 +1 +如果您访问的服务位于公开网络,您的数据有被窃取、拦截的风险。 + +2 + +3. 保护好日志。 +2 +并非所有日志都能直接传递给他人 + + +在您公开您的日志前,请先对日志中的关键信息进行抹除。 + + +部分相关服务使用 HTTP GET 请求传递数据体, +当远程服务出错时,服务对接可能会直接将此次请求的连接直接输出到日志中, +此日志可能包含了此次尝试 签名/加密 的内容, +而此内容可能包含关键信息。 + + +如果您无法分辨哪些请求需要被抹除时,您可以参考以下规则: + + + 请求连接包含大量 Hex 文本,抹除 (Hex: 由 0-9 和 ABCDEF 组成的序列 ) + 2 + + 请求包含大量 Base64 文本,抹除 (如您不知道什么是 Base64 文本,您可以简单当做是超长的英文与符号组合) + 2 + + 请求连接过长,抹除(如连接日志换行了三次都还没有显示完全) + 2 + + + diff --git a/mirai-console/backend/mirai-console/src/enduserreadme/EndUserReadme.kt b/mirai-console/backend/mirai-console/src/enduserreadme/EndUserReadme.kt new file mode 100644 index 0000000000..7cbaef001e --- /dev/null +++ b/mirai-console/backend/mirai-console/src/enduserreadme/EndUserReadme.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2019-2023 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.console.enduserreadme + +import java.util.* + +/** + * 最终用户须知 + * + * @since 2.16.0 + */ +public class EndUserReadme { + public companion object { + public const val PAUSE: String = "" + public const val DELAY: String = "" + } + + internal val pages: MutableMap = linkedMapOf() + + public class Render { + private val msgs = mutableListOf() + + @KeepDetermination + public operator fun String.unaryPlus() { + msg(this) + } + + @KeepDetermination + public operator fun plusAssign(s: String) { + msg(s) + } + + @KeepDetermination + public fun pause() { + msg(PAUSE) + } + + @KeepDetermination + public fun delay() { + msg(DELAY) + } + + /** + * 延迟一段时间 + * + * @param time 单位:秒 + */ + @KeepDetermination + public fun delay(time: Int) { + msg(DELAY + time) + } + + @KeepDetermination + public fun msg(message: String) { + msgs.add(message) + } + + public fun render(): String = msgs.joinToString(separator = "\n") + } + + @KeepDetermination + public fun put(category: String, render: Render.() -> Unit) { + pages[category] = Render().also(render).render() + } + + /** + * 同时添加多个须知定义 + * + * 格式: + * ```text + * + * ::category.c1 + * + * Here is c1 + * + * delay 2s + * 2 + * + * paused + * + * + * ::category.c1 + * + * Here is c2 + * + * ``` + */ + @KeepDetermination + public fun putAll(fullText: String) { + if (fullText.isBlank()) return + val lines = LinkedList(fullText.lines()) + + var category: String + val buffer = mutableListOf() + + while (true) { + if (lines.isEmpty()) return + val rm = lines.removeFirst() + + if (rm.isBlank()) continue + if (rm.startsWith("::")) { + category = rm.substring(2) + break + } + throw IllegalArgumentException("First non-empty line must be category define: $rm") + } + + fun flush() { + while (buffer.isNotEmpty()) { + if (buffer.first().isBlank()) { + buffer.removeAt(0) + continue + } + break + } + + + while (buffer.isNotEmpty()) { + if (buffer.last().isBlank()) { + buffer.removeAt(buffer.lastIndex) + continue + } + break + } + + pages[category] = buffer.joinToString(separator = "\n") + buffer.clear() + } + + while (lines.isNotEmpty()) { + val rm = lines.removeFirst() + if (rm.startsWith("::")) { + flush() + category = rm.substring(2) + continue + } + buffer.add(rm) + } + + flush() + } + + @DslMarker + private annotation class KeepDetermination +} \ No newline at end of file diff --git a/mirai-console/backend/mirai-console/src/events/EndUserReadmeInitializeEvent.kt b/mirai-console/backend/mirai-console/src/events/EndUserReadmeInitializeEvent.kt new file mode 100644 index 0000000000..c1d92e8e7c --- /dev/null +++ b/mirai-console/backend/mirai-console/src/events/EndUserReadmeInitializeEvent.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2019-2023 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.console.events + +import net.mamoe.mirai.console.enduserreadme.EndUserReadme +import net.mamoe.mirai.event.AbstractEvent +import net.mamoe.mirai.utils.MiraiInternalApi + +public class EndUserReadmeInitializeEvent @MiraiInternalApi constructor( + public val readme: EndUserReadme, +) : ConsoleEvent, AbstractEvent() diff --git a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt index bd9596ad7b..a016aa03ae 100644 --- a/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt +++ b/mirai-console/backend/mirai-console/src/internal/MiraiConsoleImplementationBridge.kt @@ -46,6 +46,7 @@ import net.mamoe.mirai.console.internal.data.builtins.AutoLoginConfig.Account.Pa import net.mamoe.mirai.console.internal.data.builtins.DataScope import net.mamoe.mirai.console.internal.data.builtins.LoggerConfig import net.mamoe.mirai.console.internal.data.builtins.PluginDependenciesConfig +import net.mamoe.mirai.console.internal.enduserreadme.EndUserReadmeProcessor import net.mamoe.mirai.console.internal.extension.GlobalComponentStorage import net.mamoe.mirai.console.internal.extension.GlobalComponentStorageImpl import net.mamoe.mirai.console.internal.logging.LoggerControllerImpl @@ -365,6 +366,10 @@ ___ ____ _ _____ _ mainLogger.info { "${pluginManager.plugins.count { it.isEnabled }} plugin(s) enabled." } } + phase("end-user-readme") { + EndUserReadmeProcessor.process(this) + } + phase("auto-login bots") { runBlocking { val config = DataScope.get() diff --git a/mirai-console/backend/mirai-console/src/internal/data/builtins/EndUserReadmeData.kt b/mirai-console/backend/mirai-console/src/internal/data/builtins/EndUserReadmeData.kt new file mode 100644 index 0000000000..a7a6e61d3a --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/data/builtins/EndUserReadmeData.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2023 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.console.internal.data.builtins + +import net.mamoe.mirai.console.data.PluginDataHolder +import net.mamoe.mirai.console.data.PluginDataStorage +import net.mamoe.mirai.console.data.ReadOnlyPluginConfig +import net.mamoe.mirai.console.data.value +import net.mamoe.mirai.console.util.ConsoleExperimentalApi + +@OptIn(ConsoleExperimentalApi::class) +internal class EndUserReadmeData : ReadOnlyPluginConfig("EndUserReadme") { + val data: MutableMap by value() + + private lateinit var storage_: PluginDataStorage + private lateinit var owner_: PluginDataHolder + override fun onInit(owner: PluginDataHolder, storage: PluginDataStorage) { + this.storage_ = storage + this.owner_ = owner + } + + internal fun saveNow() { + storage_.store(owner_, this) + } +} diff --git a/mirai-console/backend/mirai-console/src/internal/enduserreadme/EndUserReadmeProcessor.kt b/mirai-console/backend/mirai-console/src/internal/enduserreadme/EndUserReadmeProcessor.kt new file mode 100644 index 0000000000..0f556dd5b0 --- /dev/null +++ b/mirai-console/backend/mirai-console/src/internal/enduserreadme/EndUserReadmeProcessor.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2019-2023 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.console.internal.enduserreadme + +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import net.mamoe.mirai.console.ConsoleFrontEndImplementation +import net.mamoe.mirai.console.command.ConsoleCommandSender +import net.mamoe.mirai.console.enduserreadme.EndUserReadme +import net.mamoe.mirai.console.events.EndUserReadmeInitializeEvent +import net.mamoe.mirai.console.internal.MiraiConsoleImplementationBridge +import net.mamoe.mirai.console.internal.data.builtins.EndUserReadmeData +import net.mamoe.mirai.console.util.ConsoleInput +import net.mamoe.mirai.console.util.sendAnsiMessage +import net.mamoe.mirai.event.broadcast +import net.mamoe.mirai.utils.MiraiInternalApi +import net.mamoe.mirai.utils.sha256 +import net.mamoe.mirai.utils.toUHexString +import java.io.File +import java.net.InetAddress + +internal object EndUserReadmeProcessor { + private val PADDING = "=".repeat(100) + private fun StringBuilder.pad(size: Int) { + var size0 = size + + while (size0 > 0) { + val padded = size0.coerceAtMost(PADDING.length) + append(PADDING, 0, padded) + size0 -= padded + } + } + + private fun header(title: String): String { + val padding = 100 - title.length + + val lpadding = padding / 2 + val rpadding = padding - lpadding + + return buildString { + pad(lpadding) + append(" [ ").append(title).append(" ] ") + pad(rpadding) + } + } + + private val systemDefaultNames = hashSetOf( + "ubuntu", "debian", "arch", + "centos", "fedora", "localhost", + ) + + private fun getComputerName(): String { + System.getenv("COMPUTERNAME")?.takeUnless(String::isBlank)?.let { return it } + System.getenv("HOSTNAME")?.takeUnless(String::isBlank)?.let { return it } + + runCatching { + InetAddress.getLocalHost().hostName + ?.takeIf { it.lowercase() !in systemDefaultNames } + ?.takeUnless(String::isBlank) + ?.let { return it } + } + + runCatching { + File("/etc/machine-id").readText().takeUnless(String::isBlank)?.let { return it.trim() } + } + return "Unknown Computer" + } + + @OptIn(MiraiInternalApi::class, ConsoleFrontEndImplementation::class) + fun process(console: MiraiConsoleImplementationBridge) { + if (System.getenv("CI") == "true") return + if (System.getProperty("mirai.console.skip-end-user-readme") in listOf("", "true", "yes")) return + + val pcName = getComputerName() + val dataObject = EndUserReadmeData() + console.consoleDataScope.addAndReloadConfig(dataObject) + + + runBlocking { + val readme = EndUserReadme() + runCatching { + EndUserReadmeProcessor::class.java.getResourceAsStream("readme.txt")?.bufferedReader()?.use { + readme.putAll(it.readText()) + } + }.onFailure { console.mainLogger.error(it) } + + EndUserReadmeInitializeEvent(readme).broadcast() + + // region Remove already read + + val pcNameBCode = pcName.toByteArray() + var changed = false + + readme.pages.asSequence().map { (key, value) -> + return@map key to value.sha256() + }.onEach { (_, hash) -> + for (i in hash.indices) { + hash[i] = hash[i].toInt().xor(pcNameBCode[i % pcNameBCode.size].toInt()).toByte() + } + }.map { (k, v) -> + return@map k to v.toUHexString() + }.toList().forEach { (key, hash) -> + if (dataObject.data[key] == hash) { + readme.pages.remove(key) + } else { + dataObject.data[key] = hash + changed = true + } + } + // endregion + + suspend fun wait(seconds: Int) { + if (seconds < 1) return + + var printWaiting = true + + repeat(seconds) { counter -> + val suffix = (seconds - counter).toString() + "s" + withTimeoutOrNull(1000L) { + if (printWaiting) { + ConsoleInput.requestInput("Please wait $suffix...") + printWaiting = false + } + while (true) { + ConsoleInput.requestInput("Please read before continuing ($suffix)") + } + } + } + + } + + suspend fun pause() { + ConsoleInput.requestInput("Enter to continue") + } + + if (readme.pages.isNotEmpty()) { + listOf( + header("End User Readme"), + "最终用户须知有更新,在您继续使用前,您必须完整阅读新的用户须知。", + ).forEach { ConsoleCommandSender.sendMessage(it) } + } + + readme.pages.forEach { (category, message) -> + ConsoleCommandSender.sendMessage(header(category)) + message.lines().forEach { command -> + val ctrim = command.trim() + if (ctrim == EndUserReadme.PAUSE) { + pause() + } else if (ctrim == EndUserReadme.DELAY) { + wait(3) + } else if (ctrim.startsWith(EndUserReadme.DELAY)) { + wait(ctrim.removePrefix(EndUserReadme.DELAY).trim().toIntOrNull() ?: 3) + } else { + ConsoleCommandSender.sendAnsiMessage(command) + } + } + wait(3) + pause() + } + + + if (changed) { + dataObject.saveNow() + } + + } + } +} \ No newline at end of file diff --git a/mirai-console/frontend/mirai-console-terminal/src/JLineInputDaemon.kt b/mirai-console/frontend/mirai-console-terminal/src/JLineInputDaemon.kt index 3650071396..46cb54f16d 100644 --- a/mirai-console/frontend/mirai-console-terminal/src/JLineInputDaemon.kt +++ b/mirai-console/frontend/mirai-console-terminal/src/JLineInputDaemon.kt @@ -69,7 +69,7 @@ internal object JLineInputDaemon : Runnable { } continue } - if (nextTask.coroutine.isCancelled) continue + if (nextTask.coroutine.isCompleted) continue synchronized(queueStateChangeNoticer) { @@ -120,7 +120,7 @@ internal object JLineInputDaemon : Runnable { suspendReader(true) return@invokeOnCancellation } - if (nnextTask2.coroutine.isCancelled) continue + if (nnextTask2.coroutine.isCompleted) continue nnextTask = nnextTask2 break @@ -156,6 +156,26 @@ internal object JLineInputDaemon : Runnable { }.addLast(req) queueStateChangeNoticer.notify() + + if (crtProcessing != null && crtProcessing.coroutine.isCompleted) { + val nnextTask: Request + while (true) { + val nnextTask2 = queue.poll() ?: queueDelayable.poll() + if (nnextTask2 == null) { + nnextTask = req + break + } + if (nnextTask2.coroutine.isCompleted) continue + + nnextTask = nnextTask2 + break + } + processing = nnextTask + updateFlags(nnextTask) + if (lineReader.isReading) { + readerImpl.redisplay() + } + } } tryResumeReader(true) } diff --git a/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt b/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt index 106ff13118..e4bf2a5745 100644 --- a/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt +++ b/mirai-console/tools/gradle-plugin/src/main/kotlin/MiraiConsoleGradlePlugin.kt @@ -231,6 +231,7 @@ public class MiraiConsoleGradlePlugin : Plugin { } } runConsole.standardInput = System.`in` + runConsole.jvmArgs("-Dmirai.console.skip-end-user-readme") buildPluginTasks.forEach { runConsole.dependsOn(it.first) } miraiExtension.consoleTestRuntimeConf.forEach { it.invoke(runConsole) }