diff --git a/mirai-core-api/build.gradle.kts b/mirai-core-api/build.gradle.kts index c146bbfd59..c5bcf13c1b 100644 --- a/mirai-core-api/build.gradle.kts +++ b/mirai-core-api/build.gradle.kts @@ -44,12 +44,16 @@ kotlin { implementation(project(":mirai-console-compiler-annotations")) implementation(`kotlinx-serialization-protobuf`) implementation(`kotlinx-atomicfu`) + implementation(`jetbrains-annotations`) // runtime from mirai-core-utils relocateCompileOnly(`ktor-io_relocated`) implementation(`kotlin-jvm-blocking-bridge`) implementation(`kotlin-dynamic-delegation`) + + implementation(`log4j-api`) + compileOnly(`slf4j-api`) } } @@ -61,14 +65,6 @@ kotlin { } } - findByName("jvmBaseMain")?.apply { - dependencies { - implementation(`jetbrains-annotations`) - implementation(`log4j-api`) - compileOnly(`slf4j-api`) - } - } - afterEvaluate { findByName("androidUnitTest")?.apply { dependencies { diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt index fe1704a18a..56a0144866 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Contact.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -7,8 +7,7 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -@file:Suppress("EXPERIMENTAL_API_USAGE", "NOTHING_TO_INLINE", "EXPERIMENTAL_OVERRIDE") -@file:OptIn(JavaFriendlyAPI::class) +@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE") @file:JvmBlockingBridge package net.mamoe.mirai.contact @@ -23,16 +22,16 @@ import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* import net.mamoe.mirai.recallMessage import net.mamoe.mirai.utils.* +import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage -import kotlin.coroutines.cancellation.CancellationException -import kotlin.jvm.JvmStatic -import kotlin.jvm.JvmSynthetic +import java.io.File +import java.io.InputStream /** * 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], 和 [群][Group]. */ @NotStableForInheritance -public expect interface Contact : ContactOrBot, CoroutineScope { +public interface Contact : ContactOrBot, CoroutineScope { /** * 这个联系对象所属 [Bot]. */ @@ -67,7 +66,8 @@ public expect interface Contact : ContactOrBot, CoroutineScope { * 发送纯文本消息 * @see sendMessage */ - public open suspend fun sendMessage(message: String): MessageReceipt + public suspend fun sendMessage(message: String): MessageReceipt = + this.sendMessage(message.toPlainText()) /** * 上传一个 [资源][ExternalResource] 作为图片以备发送. @@ -88,25 +88,82 @@ public expect interface Contact : ContactOrBot, CoroutineScope { */ public suspend fun uploadImage(resource: ExternalResource): Image - @JvmBlockingBridge public companion object { + /** + * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 + * + * 注意:此函数不会关闭 [imageStream] + * + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + * @see FileCacheStrategy + */ + @JvmStatic + @JvmOverloads + public suspend fun C.sendImage( + imageStream: InputStream, + formatName: String? = null + ): MessageReceipt = imageStream.sendAsImageTo(this, formatName) + + /** + * 将文件作为图片发送到指定联系人 + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + * @see FileCacheStrategy + */ + @JvmStatic + @JvmOverloads + public suspend fun C.sendImage( + file: File, + formatName: String? = null + ): MessageReceipt = file.sendAsImageTo(this, formatName) + /** * 将资源作为单独的图片消息发送给 [this] * * @see Contact.sendMessage 最终调用, 发送消息. */ @JvmStatic - public suspend fun C.sendImage(resource: ExternalResource): MessageReceipt + public suspend fun C.sendImage(resource: ExternalResource): MessageReceipt = + resource.sendAsImageTo(this) + + + /** + * 读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送 + * + * 注意:本函数不会关闭流 + * + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmOverloads + public suspend fun Contact.uploadImage( + imageStream: InputStream, + formatName: String? = null + ): Image = imageStream.uploadAsImage(this@uploadImage, formatName) + + /** + * 将文件作为图片上传, 但不发送 + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmOverloads + public suspend fun Contact.uploadImage( + file: File, + formatName: String? = null + ): Image = file.uploadAsImage(this, formatName) /** * 将文件作为图片上传, 但不发送 * @throws OverFileSizeMaxException */ - @Throws(OverFileSizeMaxException::class, CancellationException::class) + @Throws(OverFileSizeMaxException::class) @JvmStatic @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXTENSION_SHADOWED_BY_MEMBER") @kotlin.internal.LowPriorityInOverloadResolution // for better Java API - public suspend fun Contact.uploadImage(resource: ExternalResource): Image + public suspend fun Contact.uploadImage(resource: ExternalResource): Image = this.uploadImage(resource) } } diff --git a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt index 1bf1da557f..92454e87c3 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -11,6 +11,7 @@ package net.mamoe.mirai.contact import net.mamoe.mirai.contact.file.RemoteFiles +import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.NotStableForInheritance /** @@ -23,7 +24,23 @@ import net.mamoe.mirai.utils.NotStableForInheritance * @see RemoteFiles */ @NotStableForInheritance -public expect interface FileSupported : Contact { +public interface FileSupported : Contact { + /** + * 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表. + * + * **注意:** 已弃用, 请使用 [files]. + * + * @since 2.5 + */ + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Please use files instead.", + replaceWith = ReplaceWith("files.root"), + level = DeprecationLevel.ERROR + ) // deprecated since 2.8.0-RC + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") + public val filesRoot: net.mamoe.mirai.utils.RemoteFile + /** * 获取远程文件列表 (管理器). * diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt index 436faddb5c..52886c8dd9 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -7,13 +7,18 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:JvmBlockingBridge + package net.mamoe.mirai.contact.file import kotlinx.coroutines.flow.Flow +import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.contact.PermissionDeniedException import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.JavaFriendlyAPI import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.ProgressionCallback +import java.util.stream.Stream /** * 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响. @@ -23,8 +28,9 @@ import net.mamoe.mirai.utils.ProgressionCallback * @see AbsoluteFile * @see AbsoluteFileFolder */ +@Suppress("SEALED_INHERITOR_IN_DIFFERENT_MODULE") @NotStableForInheritance -public expect interface AbsoluteFolder : AbsoluteFileFolder { +public interface AbsoluteFolder : AbsoluteFileFolder { /** * 当前快照中文件数量, 当有文件更新时(上传/删除文件) 该属性不会更新. * @@ -37,7 +43,7 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { /** * 当该目录为空时返回 `true`. */ - public open fun isEmpty(): Boolean + public fun isEmpty(): Boolean = contentsCount == 0 /** * 返回更新了文件或目录信息 ([lastModifiedTime] 等) 的, 指向相同文件的 [AbsoluteFileFolder]. @@ -58,18 +64,42 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { */ public suspend fun folders(): Flow + /** + * 获取该目录下所有子目录列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [folders]. + */ + @JavaFriendlyAPI + public suspend fun foldersStream(): Stream + /** * 获取该目录下所有文件列表. */ public suspend fun files(): Flow + /** + * 获取该目录下所有文件列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [files]. + */ + @JavaFriendlyAPI + public suspend fun filesStream(): Stream + /** * 获取该目录下所有文件和子目录列表. */ public suspend fun children(): Flow + /** + * 获取该目录下所有文件和子目录列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [children]. + */ + @JavaFriendlyAPI + public suspend fun childrenStream(): Stream + /////////////////////////////////////////////////////////////////////////// // resolve and upload /////////////////////////////////////////////////////////////////////////// @@ -101,6 +131,8 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { /** * 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找. */ + @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI + @JvmOverloads public suspend fun resolveFileById( id: String, deep: Boolean = false @@ -113,6 +145,16 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { path: String ): Flow + /** + * 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveFiles]. + */ + @JavaFriendlyAPI + public suspend fun resolveFilesStream( + path: String + ): Stream + /** * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. */ @@ -120,6 +162,16 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { path: String ): Flow + /** + * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveAll]. + */ + @JavaFriendlyAPI + public suspend fun resolveAllStream( + path: String + ): Stream + /** * 上传一个文件到该目录, 返回上传成功的文件标识. * @@ -137,6 +189,8 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { * * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) */ + @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI + @JvmOverloads public suspend fun uploadNewFile( filepath: String, content: ExternalResource, @@ -148,7 +202,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder { * 根目录 folder ID. * @see id */ - @Suppress("CONST_VAL_WITHOUT_INITIALIZER") // compiler bug - public const val ROOT_FOLDER_ID: String + public const val ROOT_FOLDER_ID: String = "/" } } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt b/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt index fd5e7db4a5..e8e82d40d9 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/roaming/RoamingMessages.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -7,11 +7,18 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ + +@file:JvmBlockingBridge + package net.mamoe.mirai.contact.roaming import kotlinx.coroutines.flow.Flow +import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSource +import net.mamoe.mirai.utils.JavaFriendlyAPI +import net.mamoe.mirai.utils.JdkStreamSupport.toStream +import java.util.stream.Stream /** * 漫游消息记录管理器. 可通过 [RoamingSupported.roamingMessages] 获得. @@ -19,13 +26,13 @@ import net.mamoe.mirai.message.data.MessageSource * @since 2.8 * @see RoamingSupported */ -public expect interface RoamingMessages { +public interface RoamingMessages { /////////////////////////////////////////////////////////////////////////// // Get list /////////////////////////////////////////////////////////////////////////// /** - * 查询指定时间段内的漫游消息记录. + * 查询指定时间段内的漫游消息记录. Java Stream 方法查看 [getMessagesStream]. * * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. @@ -42,6 +49,7 @@ public expect interface RoamingMessages { * @param timeEnd 结束时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值. * @param filter 过滤器. */ + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI public suspend fun getMessagesIn( timeStart: Long, timeEnd: Long, @@ -49,7 +57,7 @@ public expect interface RoamingMessages { ): Flow /** - * 查询所有漫游消息记录. + * 查询所有漫游消息记录. Java Stream 方法查看 [getAllMessagesStream]. * * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. @@ -64,7 +72,58 @@ public expect interface RoamingMessages { * * @param filter 过滤器. */ - public open suspend fun getAllMessages( + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI + public suspend fun getAllMessages( filter: RoamingMessageFilter? = null - ): Flow + ): Flow = getMessagesIn(0, Long.MAX_VALUE, filter) + + /** + * 查询指定时间段内的漫游消息记录. Kotlin Flow 版本查看 [getMessagesIn]. + * + * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. + * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. + * + * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. + * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.get(MessageSource.Key)`), 判断 [MessageSource.fromId] (发送人). + * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). + * + * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. + * + * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. + * + * @param timeStart 起始时间戳, 单位为秒. 可以为 `0`, 即表示从可以获取的最早的消息起. 负数将会被看是 `0`. + * @param timeEnd 结束时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值. + * @param filter 过滤器. + */ + @Suppress("OVERLOADS_INTERFACE") + @JvmOverloads + @JavaFriendlyAPI + public suspend fun getMessagesStream( + timeStart: Long, + timeEnd: Long, + filter: RoamingMessageFilter? = null + ): Stream = getMessagesIn(timeStart, timeEnd, filter).toStream() + + /** + * 查询所有漫游消息记录. Kotlin Flow 版本查看 [getAllMessages]. + * + * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. + * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. + * + * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. + * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.get(MessageSource.Key)`), 判断 [MessageSource.fromId] (发送人). + * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). + * + * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. + * + * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. + * + * @param filter 过滤器. + */ + @Suppress("OVERLOADS_INTERFACE") + @JvmOverloads + @JavaFriendlyAPI + public suspend fun getAllMessagesStream( + filter: RoamingMessageFilter? = null + ): Stream = getMessagesStream(0, Long.MAX_VALUE, filter) } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt b/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt index 0a3575d5f2..70fee1e0f2 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/EventChannel.kt @@ -25,14 +25,11 @@ import net.mamoe.mirai.IMirai import net.mamoe.mirai.event.ConcurrencyKind.CONCURRENT import net.mamoe.mirai.event.ConcurrencyKind.LOCKED import net.mamoe.mirai.event.events.BotEvent -import net.mamoe.mirai.utils.MiraiInternalApi -import net.mamoe.mirai.utils.NotStableForInheritance -import net.mamoe.mirai.utils.context +import net.mamoe.mirai.internal.event.JvmMethodListenersInternal +import net.mamoe.mirai.utils.* +import java.util.function.Consumer import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext -import kotlin.jvm.JvmMultifileClass -import kotlin.jvm.JvmName -import kotlin.jvm.JvmSynthetic import kotlin.reflect.KClass /** @@ -82,18 +79,13 @@ import kotlin.reflect.KClass * 使用 [EventChannel.forwardToChannel] 可将事件转发到指定 [SendChannel]. */ @NotStableForInheritance // since 2.12, before it was `final class`. -public expect abstract class EventChannel @MiraiInternalApi public constructor( - baseEventClass: KClass, - - defaultCoroutineContext: CoroutineContext, -) { +public abstract class EventChannel @MiraiInternalApi public constructor( + public val baseEventClass: KClass, /** * 此事件通道的默认 [CoroutineScope.coroutineContext]. 将会被添加给所有注册的事件监听器. */ - public val defaultCoroutineContext: CoroutineContext - - public val baseEventClass: KClass - + public val defaultCoroutineContext: CoroutineContext, +) { /** * 创建事件监听并将监听结果转发到 [channel]. 当 [Channel.send] 抛出 [ClosedSendChannelException] 时停止 [Listener] 监听和转发. * @@ -120,7 +112,16 @@ public expect abstract class EventChannel @MiraiInternalA channel: SendChannel<@UnsafeVariance BaseEvent>, coroutineContext: CoroutineContext = EmptyCoroutineContext, priority: EventPriority = EventPriority.MONITOR, - ): Listener<@UnsafeVariance BaseEvent> + ): Listener<@UnsafeVariance BaseEvent> { + return subscribe(baseEventClass, coroutineContext, priority = priority) { + try { + channel.send(it) + ListeningStatus.LISTENING + } catch (_: ClosedSendChannelException) { + ListeningStatus.STOPPED + } + } + } /** * 通过 [Flow] 接收此通道内的所有事件. @@ -188,7 +189,9 @@ public expect abstract class EventChannel @MiraiInternalA * @see filterIsInstance 过滤指定类型的事件 */ @JvmSynthetic - public fun filter(filter: suspend (event: BaseEvent) -> Boolean): EventChannel + public fun filter(filter: suspend (event: BaseEvent) -> Boolean): EventChannel { + return FilterEventChannel(this, filter) + } /** * [EventChannel.filter] 的 Java 版本. @@ -228,20 +231,32 @@ public expect abstract class EventChannel @MiraiInternalA */ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution - public fun filter(filter: (event: BaseEvent) -> Boolean): EventChannel + public fun filter(filter: (event: BaseEvent) -> Boolean): EventChannel { + return filter { runBIO { filter(it) } } + } /** * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] * @see filter 获取更多信息 */ @JvmSynthetic - public inline fun filterIsInstance(): EventChannel + public inline fun filterIsInstance(): EventChannel = + filterIsInstance(E::class) + + /** + * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] + * @see filter 获取更多信息 + */ + public fun filterIsInstance(kClass: KClass): EventChannel { + return filter { kClass.isInstance(it) }.cast() + } /** * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] * @see filter 获取更多信息 */ - public fun filterIsInstance(kClass: KClass): EventChannel + public fun filterIsInstance(clazz: Class): EventChannel = + filterIsInstance(clazz.kotlin) /** @@ -258,14 +273,30 @@ public expect abstract class EventChannel @MiraiInternalA */ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") @kotlin.internal.LowPriorityInOverloadResolution - public fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel + public fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel { + return context(coroutineExceptionHandler) + } /** * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] * @see context */ - public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel + public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel { + return context(CoroutineExceptionHandler { _, throwable -> + coroutineExceptionHandler(throwable) + }) + } + /** + * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] + * @see context + * @since 2.12 + */ + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public fun exceptionHandler(coroutineExceptionHandler: Consumer): EventChannel { + return exceptionHandler { coroutineExceptionHandler.accept(it) } + } /** * 将 [coroutineScope] 作为这个 [EventChannel] 的父作用域. @@ -279,7 +310,9 @@ public expect abstract class EventChannel @MiraiInternalA * * @see CoroutineScope.globalEventChannel `GlobalEventChannel.parentScope()` 的扩展 */ - public fun parentScope(coroutineScope: CoroutineScope): EventChannel + public fun parentScope(coroutineScope: CoroutineScope): EventChannel { + return context(coroutineScope.coroutineContext) + } /** * 指定协程父 [Job]. 之后在此 [EventChannel] 下创建的事件监听器都会成为 [job] 的子任务, 当 [job] 被取消时, 所有的事件监听器都会被取消. @@ -289,7 +322,9 @@ public expect abstract class EventChannel @MiraiInternalA * @see parentScope * @see context */ - public fun parentJob(job: Job): EventChannel + public fun parentJob(job: Job): EventChannel { + return context(job) + } // endregion @@ -390,7 +425,7 @@ public expect abstract class EventChannel @MiraiInternalA concurrency: ConcurrencyKind = LOCKED, priority: EventPriority = EventPriority.NORMAL, noinline handler: suspend E.(E) -> ListeningStatus, - ): Listener + ): Listener = subscribe(E::class, coroutineContext, concurrency, priority, handler) /** * 与 [subscribe] 的区别是接受 [eventClass] 参数, 而不使用 `reified` 泛型. 通常推荐使用具体化类型参数. @@ -405,7 +440,10 @@ public expect abstract class EventChannel @MiraiInternalA concurrency: ConcurrencyKind = LOCKED, priority: EventPriority = EventPriority.NORMAL, handler: suspend E.(E) -> ListeningStatus, - ): Listener + ): Listener = subscribeInternal( + eventClass, + createListener0(coroutineContext, concurrency, priority) { it.handler(it); } + ) /** * 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件. @@ -427,7 +465,7 @@ public expect abstract class EventChannel @MiraiInternalA concurrency: ConcurrencyKind = CONCURRENT, priority: EventPriority = EventPriority.NORMAL, noinline handler: suspend E.(E) -> Unit, - ): Listener + ): Listener = subscribeAlways(E::class, coroutineContext, concurrency, priority, handler) /** @@ -441,7 +479,10 @@ public expect abstract class EventChannel @MiraiInternalA concurrency: ConcurrencyKind = CONCURRENT, priority: EventPriority = EventPriority.NORMAL, handler: suspend E.(E) -> Unit, - ): Listener + ): Listener = subscribeInternal( + eventClass, + createListener0(coroutineContext, concurrency, priority) { it.handler(it); ListeningStatus.LISTENING } + ) /** * 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件, 只监听一次. @@ -459,7 +500,7 @@ public expect abstract class EventChannel @MiraiInternalA coroutineContext: CoroutineContext = EmptyCoroutineContext, priority: EventPriority = EventPriority.NORMAL, noinline handler: suspend E.(E) -> Unit, - ): Listener + ): Listener = subscribeOnce(E::class, coroutineContext, priority, handler) /** * @see subscribeOnce @@ -469,7 +510,190 @@ public expect abstract class EventChannel @MiraiInternalA coroutineContext: CoroutineContext = EmptyCoroutineContext, priority: EventPriority = EventPriority.NORMAL, handler: suspend E.(E) -> Unit, - ): Listener + ): Listener = subscribeInternal( + eventClass, + createListener0(coroutineContext, ConcurrencyKind.LOCKED, priority) { it.handler(it); ListeningStatus.STOPPED } + ) + + // endregion + + /** + * 注册 [ListenerHost] 中的所有 [EventHandler] 标注的方法到这个 [EventChannel]. 查看 [EventHandler]. + * + * @param coroutineContext 在 [defaultCoroutineContext] 的基础上, 给事件监听协程的额外的 [CoroutineContext] + * + * @see subscribe + * @see EventHandler + * @see ListenerHost + */ + @JvmOverloads + public fun registerListenerHost( + host: ListenerHost, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + ) { + val jobOfListenerHost: Job? + val coroutineContext0 = if (host is SimpleListenerHost) { + val listenerCoroutineContext = host.coroutineContext + val listenerJob = listenerCoroutineContext[Job] + + val rsp = listenerCoroutineContext.minusKey(Job) + + coroutineContext + + (listenerCoroutineContext[CoroutineExceptionHandler] ?: EmptyCoroutineContext) + + val registerCancelHook = when { + listenerJob === null -> false + + // Registering cancellation hook is needless + // if [Job] of [EventChannel] is same as [Job] of [SimpleListenerHost] + (rsp[Job] ?: this.defaultCoroutineContext[Job]) === listenerJob -> false + + else -> true + } + + jobOfListenerHost = if (registerCancelHook) { + listenerCoroutineContext[Job] + } else { + null + } + rsp + } else { + jobOfListenerHost = null + coroutineContext + } + for (method in host.javaClass.declaredMethods) { + method.getAnnotation(EventHandler::class.java)?.let { + val listener = + JvmMethodListenersInternal.registerEventHandler(method, host, this, it, coroutineContext0) + // For [SimpleListenerHost.cancelAll] + jobOfListenerHost?.invokeOnCompletion { exception -> + listener.cancel( + when (exception) { + is CancellationException -> exception + is Throwable -> CancellationException(null, exception) + else -> null + } + ) + } + } + } + } + + // region Java API + + /** + * Java API. 查看 [subscribeAlways] 获取更多信息. + * + * ```java + * eventChannel.subscribeAlways(GroupMessageEvent.class, (event) -> { }); + * ``` + * + * @see subscribe + * @see subscribeAlways + */ + @JvmOverloads + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public fun subscribeAlways( + eventClass: Class, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + concurrency: ConcurrencyKind = CONCURRENT, + priority: EventPriority = EventPriority.NORMAL, + handler: Consumer, + ): Listener = subscribeInternal( + eventClass.kotlin, + createListener0(coroutineContext, concurrency, priority) { event -> + runInterruptible(Dispatchers.IO) { handler.accept(event) } + ListeningStatus.LISTENING + } + ) + + /** + * Java API. 查看 [subscribe] 获取更多信息. + * + * ```java + * eventChannel.subscribe(GroupMessageEvent.class, (event) -> { + * return ListeningStatus.LISTENING; + * }); + * ``` + * + * @see subscribe + */ + @JvmOverloads + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public fun subscribe( + eventClass: Class, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + concurrency: ConcurrencyKind = CONCURRENT, + priority: EventPriority = EventPriority.NORMAL, + handler: java.util.function.Function, + ): Listener = subscribeInternal( + eventClass.kotlin, + createListener0(coroutineContext, concurrency, priority) { event -> + runInterruptible(Dispatchers.IO) { handler.apply(event) } + } + ) + + /** + * Java API. 查看 [subscribeOnce] 获取更多信息. + * + * ```java + * eventChannel.subscribeOnce(GroupMessageEvent.class, (event) -> { }); + * ``` + * + * @see subscribe + * @see subscribeOnce + */ + @JvmOverloads + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @kotlin.internal.LowPriorityInOverloadResolution + public fun subscribeOnce( + eventClass: Class, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + concurrency: ConcurrencyKind = CONCURRENT, + priority: EventPriority = EventPriority.NORMAL, + handler: Consumer, + ): Listener = subscribeInternal( + eventClass.kotlin, + createListener0(coroutineContext, concurrency, priority) { event -> + runInterruptible(Dispatchers.IO) { handler.accept(event) } + ListeningStatus.STOPPED + } + ) + + // endregion + + // region deprecated + + /** + * 创建事件监听并将监听结果发送在 [Channel]. 将返回值 [Channel] [关闭][Channel.close] 时将会同时关闭事件监听. + * + * ## 已弃用 + * + * 请使用 [forwardToChannel] 替代. + * + * @param capacity Channel 容量. 详见 [Channel] 构造. + * + * @see subscribeAlways + * @see Channel + */ + @Deprecated( + "Please use forwardToChannel instead.", + replaceWith = ReplaceWith( + "Channel(capacity).apply { forwardToChannel(this, coroutineContext, priority) }", + "kotlinx.coroutines.channels.Channel" + ), + level = DeprecationLevel.ERROR, + ) + @DeprecatedSinceMirai(warningSince = "2.10", errorSince = "2.14") + @MiraiExperimentalApi + public fun asChannel( + capacity: Int = Channel.RENDEZVOUS, + coroutineContext: CoroutineContext = EmptyCoroutineContext, + @Suppress("UNUSED_PARAMETER") concurrency: ConcurrencyKind = CONCURRENT, + priority: EventPriority = EventPriority.NORMAL, + ): Channel = + Channel(capacity).apply { forwardToChannel(this, coroutineContext, priority) } // endregion @@ -481,7 +705,16 @@ public expect abstract class EventChannel @MiraiInternalA protected abstract fun registerListener(eventClass: KClass, listener: Listener) // to overcome visibility issue - internal fun registerListener0(eventClass: KClass, listener: Listener) + @OptIn(MiraiInternalApi::class) + internal fun registerListener0(eventClass: KClass, listener: Listener) { + return registerListener(eventClass, listener) + } + + @OptIn(MiraiInternalApi::class) + private fun , E : Event> subscribeInternal(eventClass: KClass, listener: L): L { + registerListener(eventClass, listener) + return listener + } /** * Creates [Listener] instance using the [listenerBlock] action. @@ -496,12 +729,13 @@ public expect abstract class EventChannel @MiraiInternalA ): Listener // to overcome visibility issue + @OptIn(MiraiInternalApi::class) internal fun createListener0( coroutineContext: CoroutineContext, concurrencyKind: ConcurrencyKind, priority: EventPriority, listenerBlock: suspend (E) -> ListeningStatus, - ): Listener + ): Listener = createListener(coroutineContext, concurrencyKind, priority, listenerBlock) // endregion } diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/JvmMethodListeners.kt b/mirai-core-api/src/commonMain/kotlin/event/JvmMethodListeners.kt similarity index 99% rename from mirai-core-api/src/jvmBaseMain/kotlin/event/JvmMethodListeners.kt rename to mirai-core-api/src/commonMain/kotlin/event/JvmMethodListeners.kt index e5416c6e11..15e20c77d3 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/JvmMethodListeners.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/JvmMethodListeners.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt b/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt index 571fa5a87a..3bddef9e8d 100644 --- a/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/MessageSelectBuilderUnit.kt @@ -24,11 +24,111 @@ import net.mamoe.mirai.utils.MiraiInternalApi * @see MessageSubscribersBuilder 查看上层 API */ @OptIn(MiraiInternalApi::class) -public expect abstract class MessageSelectBuilderUnit @PublishedApi internal constructor( +public abstract class MessageSelectBuilderUnit @PublishedApi internal constructor( ownerMessagePacket: M, stub: Any?, subscriber: (M.(String) -> Boolean, MessageListener) -> Unit -) : CommonMessageSelectBuilderUnit +) : CommonMessageSelectBuilderUnit(ownerMessagePacket, stub, subscriber) { + @JvmName("timeout-ncvN2qU") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker { + return timeout(timeoutMillis) + } + + @Suppress("unused") + @JvmName("invoke-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) { + return invoke(block) + } + + @Suppress("unused") + @JvmName("invoke-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? { + invoke(block) + return null + } + + @JvmName("reply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) { + return reply(block) + } + + @JvmName("reply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? { + reply(block) + return null + } + + @JvmName("reply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(message: String) { + return reply(message) + } + + @JvmName("reply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? { + reply(message) + return null + } + + @JvmName("reply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) { + return reply(message) + } + + @JvmName("reply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? { + reply(message) + return null + } + + + @JvmName("quoteReply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) { + return reply(block) + } + + @JvmName("quoteReply-RNyhSv4") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? { + reply(block) + return null + } + + @JvmName("quoteReply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) { + return reply(message) + } + + @JvmName("quoteReply-sCZ5gAI") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? { + reply(message) + return null + } + + @JvmName("quoteReply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) { + return reply(message) + } + + @JvmName("quoteReply-AVDwu3U") + @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) + public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? { + reply(message) + return null + } +} /** * [MessageSelectBuilderUnit] 的跨平台实现 @@ -194,6 +294,7 @@ public abstract class CommonMessageSelectBuilderUnit protec Unit -> { } + is Message -> ownerMessagePacket.subject.sendMessage(result) else -> ownerMessagePacket.subject.sendMessage(result.toString()) } @@ -204,6 +305,7 @@ public abstract class CommonMessageSelectBuilderUnit protec Unit -> { } + is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result) else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString()) } diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt b/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt similarity index 99% rename from mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt rename to mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt index 3676d67c42..507c36f9a4 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEvent.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt b/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt similarity index 98% rename from mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt rename to mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt index 94aa5064a0..6847587896 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.nextEventAsync.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/deprecated.nextEventAsync.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt b/mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt similarity index 99% rename from mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt rename to mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt index 1c6a30491c..9739120fa3 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/deprecated.syncFromEvent.kt +++ b/mirai-core-api/src/commonMain/kotlin/event/deprecated.syncFromEvent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/commonMain/kotlin/internal/event/JvmMethodListenersInternal.kt b/mirai-core-api/src/commonMain/kotlin/internal/event/JvmMethodListenersInternal.kt new file mode 100644 index 0000000000..86382581c5 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/internal/event/JvmMethodListenersInternal.kt @@ -0,0 +1,198 @@ +/* + * 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.internal.event + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.mamoe.mirai.event.* +import net.mamoe.mirai.utils.EventListenerLikeJava +import net.mamoe.mirai.utils.castOrNull +import java.lang.reflect.Method +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.KClass +import kotlin.reflect.full.IllegalCallableAccessException +import kotlin.reflect.full.callSuspend +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.kotlinFunction + +internal object JvmMethodListenersInternal { + + private fun isKotlinFunction(method: Method): Boolean { + + if (method.getDeclaredAnnotation(EventListenerLikeJava::class.java) != null) return false + if (method.declaringClass.getDeclaredAnnotation(EventListenerLikeJava::class.java) != null) return false + + @Suppress("RemoveRedundantQualifierName") // for strict + return method.declaringClass.getDeclaredAnnotation(Metadata::class.java) != null + } + + @Suppress("UNCHECKED_CAST") + internal fun registerEventHandler( + method: Method, + owner: Any, + eventChannel: EventChannel<*>, + annotation: EventHandler, + coroutineContext: CoroutineContext, + ): Listener { + method.isAccessible = true + val kotlinFunction = kotlin.runCatching { method.kotlinFunction }.getOrNull() + return if (kotlinFunction != null && isKotlinFunction(method)) { + // kotlin functions + + val param = kotlinFunction.parameters + when (param.size) { + 3 -> { // ownerClass, receiver, event + check(param[1].type == param[2].type) { "Illegal kotlin function ${kotlinFunction.name}. Receiver and param must have same type" } + check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { + "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" + } + } + + 2 -> { // ownerClass, event + check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { + "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" + } + } + + else -> error("function ${kotlinFunction.name} must have one Event param") + } + lateinit var listener: Listener<*> + kotlin.runCatching { + kotlinFunction.isAccessible = true + } + suspend fun callFunction(event: Event): Any? { + try { + return when (param.size) { + 3 -> { + if (kotlinFunction.isSuspend) { + kotlinFunction.callSuspend(owner, event, event) + } else withContext(Dispatchers.IO) { // for safety + kotlinFunction.call(owner, event, event) + } + + } + + 2 -> { + if (kotlinFunction.isSuspend) { + kotlinFunction.callSuspend(owner, event) + } else withContext(Dispatchers.IO) { // for safety + kotlinFunction.call(owner, event) + } + } + + else -> error("stub") + } + } catch (e: IllegalCallableAccessException) { + listener.completeExceptionally(e) + return ListeningStatus.STOPPED + } catch (e: Throwable) { + throw ExceptionInEventHandlerException(event, cause = e) + } + } + require(!kotlinFunction.returnType.isMarkedNullable) { + "Kotlin event handlers cannot have nullable return type." + } + require(kotlinFunction.parameters.none { it.type.isMarkedNullable }) { + "Kotlin event handlers cannot have nullable parameter type." + } + when (kotlinFunction.returnType.classifier) { + Unit::class, Nothing::class -> { + eventChannel.subscribeAlways( + param[1].type.classifier as KClass, + coroutineContext, + annotation.concurrency, + annotation.priority + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + callFunction(this) + } + } else callFunction(this) + }.also { listener = it } + } + + ListeningStatus::class -> { + eventChannel.subscribe( + param[1].type.classifier as KClass, + coroutineContext, + annotation.concurrency, + annotation.priority + ) { + if (annotation.ignoreCancelled) { + if ((this as? CancellableEvent)?.isCancelled != true) { + callFunction(this) as ListeningStatus + } else ListeningStatus.LISTENING + } else callFunction(this) as ListeningStatus + }.also { listener = it } + } + + else -> error("Illegal method return type. Required Void, Nothing or ListeningStatus, found ${kotlinFunction.returnType.classifier}") + } + } else { + // java methods + + val paramType = method.parameterTypes[0] + check(method.parameterTypes.size == 1 && Event::class.java.isAssignableFrom(paramType)) { + "Illegal method parameter. Required one exact Event subclass. found ${method.parameterTypes.contentToString()}" + } + suspend fun callMethod(event: Event): Any? { + fun Method.invokeWithErrorReport(self: Any?, vararg args: Any?): Any? = try { + invoke(self, *args) + } catch (exception: IllegalArgumentException) { + throw IllegalArgumentException( + "Internal Error: $exception, method=${this}, this=$self, arguments=$args, please report to https://github.com/mamoe/mirai", + exception + ) + } catch (e: Throwable) { + throw ExceptionInEventHandlerException(event, cause = e) + } + + + return if (annotation.ignoreCancelled) { + if (event.castOrNull()?.isCancelled != true) { + withContext(Dispatchers.IO) { + method.invokeWithErrorReport(owner, event) + } + } else ListeningStatus.LISTENING + } else withContext(Dispatchers.IO) { + method.invokeWithErrorReport(owner, event) + } + } + + when (method.returnType) { + Void::class.java, Void.TYPE, Nothing::class.java -> { + eventChannel.subscribeAlways( + paramType.kotlin as KClass, + coroutineContext, + annotation.concurrency, + annotation.priority + ) { + callMethod(this) + } + } + + ListeningStatus::class.java -> { + eventChannel.subscribe( + paramType.kotlin as KClass, + coroutineContext, + annotation.concurrency, + annotation.priority + ) { + callMethod(this) as ListeningStatus? + ?: error("Java method EventHandler cannot return `null`: $this") + } + } + + else -> error("Illegal method return type. Required Void or ListeningStatus, but found ${method.returnType.canonicalName}") + } + } + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt b/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt index a0f5eff8df..eb584b057c 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt @@ -15,6 +15,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.overwriteWith +import kotlinx.serialization.modules.polymorphic import net.mamoe.mirai.Mirai import net.mamoe.mirai.message.MessageSerializers import net.mamoe.mirai.message.data.MessageChain @@ -22,8 +23,9 @@ import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.SingleMessage import net.mamoe.mirai.utils.* -import kotlin.jvm.Synchronized import kotlin.reflect.KClass +import kotlin.reflect.full.allSuperclasses +import kotlin.reflect.full.isSubclassOf @MiraiInternalApi public open class MessageSourceSerializerImpl(serialName: String) : @@ -84,10 +86,22 @@ internal object MessageSerializersImpl : MessageSerializers { } } -internal expect fun SerializersModule.overwritePolymorphicWith( +internal fun SerializersModule.overwritePolymorphicWith( type: KClass, serializer: KSerializer -): SerializersModule +): SerializersModule { + return overwriteWith(SerializersModule { + // contextual(type, serializer) + for (superclass in type.allSuperclasses) { + if (superclass.isFinal) continue + if (!superclass.isSubclassOf(SingleMessage::class)) continue + @Suppress("UNCHECKED_CAST") + polymorphic(superclass as KClass) { + subclass(type, serializer) + } + } + }) +} //private inline fun SerializersModuleBuilder.hierarchicallyPolymorphic(serializer: KSerializer) = // hierarchicallyPolymorphic(M::class, serializer) diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceImpls.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceImpls.kt similarity index 100% rename from mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceImpls.kt rename to mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceImpls.kt diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt similarity index 98% rename from mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt rename to mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt index f656e52fdd..f59cbe499b 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/LoggerAdapterImpls.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/LoggerAdapterImpls.kt similarity index 100% rename from mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/LoggerAdapterImpls.kt rename to mirai-core-api/src/commonMain/kotlin/internal/utils/LoggerAdapterImpls.kt diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/Marker.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/Marker.kt index bf8908f2f3..cd0482ab17 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/Marker.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/Marker.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -9,7 +9,12 @@ package net.mamoe.mirai.internal.utils -internal expect interface Marker { - fun addParents(vararg parent: Marker) -} +import org.apache.logging.log4j.MarkerManager +internal typealias Marker = org.apache.logging.log4j.Marker + +internal object MarkerManager { + fun getMarker(name: String): Marker { + return MarkerManager.getMarker(name) + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/MarkerManager.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/MarkerManager.kt deleted file mode 100644 index 241cee0255..0000000000 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/MarkerManager.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2019-2022 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.internal.utils - -internal expect object MarkerManager { - fun getMarker(name: String): Marker -} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/message/action/AsyncRecallResult.kt b/mirai-core-api/src/commonMain/kotlin/message/action/AsyncRecallResult.kt index 684f719475..193ea3950d 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/action/AsyncRecallResult.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/action/AsyncRecallResult.kt @@ -7,22 +7,24 @@ * https://github.com/mamoe/mirai/blob/master/LICENSE */ -@file:Suppress("MemberVisibilityCanBePrivate", "unused") @file:JvmBlockingBridge package net.mamoe.mirai.message.action +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred +import kotlinx.coroutines.future.asCompletableFuture import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.MessageSource.Key.recallIn +import java.util.concurrent.CompletableFuture /** * 异步撤回结果. * * 可由 [MessageSource.recallIn] 返回得到. * - * ## 用法 + * ## Kotlin 用法示例 * * ### 获取撤回失败时的异常 * @@ -37,32 +39,58 @@ import net.mamoe.mirai.message.data.MessageSource.Key.recallIn * * 若仅需要了解撤回是否成功而不需要获取详细异常实例, 可使用 [isSuccess] * + * ## Java 用法示例 + * + * ```java + * Throwable exception = result.exceptionFuture.get(); // 阻塞线程并等待撤回的结果. + * if (exception == null) { + * // 撤回成功 + * } else { + * // 撤回失败 + * } + * ``` + * * @see MessageSource.recallIn */ -public expect class AsyncRecallResult internal constructor( +public class AsyncRecallResult internal constructor( /** - * 撤回时产生的异常, 当撤回成功时为 `null`. + * 撤回时产生的异常, 当撤回成功时为 `null`. Kotlin [Deferred] API. */ - exception: Deferred, + public val exception: Deferred, ) { /** - * 撤回失败时的异常, 当撤回成功时为 `null`. + * 撤回时产生的异常, 当撤回成功时为 `null`. Java [CompletableFuture] API. + */ + public val exceptionFuture: CompletableFuture by lazy { exception.asCompletableFuture() } + + /** + * 撤回是否成功. Kotlin [Deferred] API. */ - public val exception: Deferred + public val isSuccess: Deferred by lazy { + CompletableDeferred().apply { + exception.invokeOnCompletion { + complete(it == null) + } + } + } /** - * 撤回是否成功. + * 撤回是否成功. Java [CompletableFuture] API. */ - public val isSuccess: Deferred + public val isSuccessFuture: CompletableFuture by lazy { isSuccess.asCompletableFuture() } /** - * 挂起协程直到撤回完成, 返回撤回时产生的异常. 当撤回成功时返回 `null`. + * 挂起协程 (在 Java 为阻塞线程) 直到撤回完成, 返回撤回时产生的异常. 当撤回成功时返回 `null`. */ - public suspend fun awaitException(): Throwable? + public suspend fun awaitException(): Throwable? { + return exception.await() + } /** - * 挂起协程直到撤回完成, 返回撤回的结果. + * 挂起协程 (在 Java 为阻塞线程) 直到撤回完成, 返回撤回的结果. */ - public suspend fun awaitIsSuccess(): Boolean + public suspend fun awaitIsSuccess(): Boolean { + return isSuccess.await() + } } diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt index 2eca81f1be..40bcf791a9 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt @@ -21,11 +21,9 @@ import net.mamoe.mirai.contact.FileSupported import net.mamoe.mirai.contact.file.AbsoluteFile import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.message.code.CodableMessage +import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode import net.mamoe.mirai.message.data.visitor.MessageVisitor -import net.mamoe.mirai.utils.MiraiExperimentalApi -import net.mamoe.mirai.utils.MiraiInternalApi -import net.mamoe.mirai.utils.NotStableForInheritance -import net.mamoe.mirai.utils.map +import net.mamoe.mirai.utils.* /** * 文件消息. @@ -43,11 +41,10 @@ import net.mamoe.mirai.utils.map * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. */ @Serializable(FileMessage.Serializer::class) -@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST") @SerialName(FileMessage.SERIAL_NAME) @NotStableForInheritance @JvmBlockingBridge -public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { +public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { /** * 服务器需要的某种 ID. */ @@ -68,10 +65,30 @@ public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMe */ public val size: Long - open override fun contentToString(): String + override fun contentToString(): String = "[文件]$name" // orthodox @MiraiExperimentalApi - open override fun appendMiraiCodeTo(builder: StringBuilder) + override fun appendMiraiCodeTo(builder: StringBuilder) { + builder.append("[mirai:file:") + builder.appendStringAsMiraiCode(id).append(",") + builder.append(internalId).append(",") + builder.appendStringAsMiraiCode(name).append(",") + builder.append(size).append("]") + } + + /** + * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. + */ + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Please use toAbsoluteFile", + ReplaceWith("this.toAbsoluteFile(contact)"), + level = DeprecationLevel.ERROR + ) // deprecated since 2.8.0-RC + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") + public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { + return contact.filesRoot.resolveById(id) + } /** * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`. @@ -80,29 +97,34 @@ public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMe */ public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? - open override val key: Key + override val key: Key get() = Key @MiraiInternalApi - open override fun accept(visitor: MessageVisitor, data: D): R + override fun accept(visitor: MessageVisitor, data: D): R { + return visitor.visitFileMessage(this, data) + } /** * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. */ public companion object Key : - AbstractPolymorphicMessageKey { + AbstractPolymorphicMessageKey( + MessageContent, { it.safeCast() }) { - @Suppress("CONST_VAL_WITHOUT_INITIALIZER") - public const val SERIAL_NAME: String + public const val SERIAL_NAME: String = "FileMessage" /** * 构造 [FileMessage] * @since 2.5 */ @JvmStatic - public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage + public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = + Mirai.createFileMessage(id, internalId, name, size) } - public object Serializer : KSerializer // not polymorphic + + public object Serializer : + KSerializer by @OptIn(MiraiInternalApi::class) FallbackFileMessageSerializer() } @MiraiInternalApi diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MessageChain.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MessageChain.kt index 2b92af3edb..2a9516b879 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/MessageChain.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/MessageChain.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -34,8 +34,9 @@ import net.mamoe.mirai.message.data.MessageSource.Key.recall import net.mamoe.mirai.message.data.MessageSource.Key.recallIn import net.mamoe.mirai.message.data.visitor.MessageVisitor import net.mamoe.mirai.utils.* -import kotlin.jvm.* +import java.util.stream.Stream import kotlin.reflect.KProperty +import kotlin.streams.asSequence import net.mamoe.mirai.console.compiler.common.ResolveContext.Kind.RESTRICTED_ABSTRACT_MESSAGE_KEYS as RAMK /** @@ -553,6 +554,11 @@ public fun Message.toMessageChain(): MessageChain = when (this) { else -> error("Message is either MessageChain nor SingleMessage: $this") } +/** + * 扁平化 [this] 并创建一个 [MessageChain]. + */ +@JvmName("newChain") +public fun Stream.toMessageChain(): MessageChain = this.asSequence().toMessageChain() // region delegate diff --git a/mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt b/mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt index 769873f374..bcd81c66cd 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/AbstractBotConfiguration.kt @@ -10,29 +10,144 @@ package net.mamoe.mirai.utils import net.mamoe.mirai.Bot -import kotlin.jvm.JvmOverloads +import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo +import java.io.File +import java.io.InputStream /** - * [BotConfiguration] 的平台特别配置 + * [BotConfiguration] 的 JVM 平台特别配置 * @since 2.15 */ @NotStableForInheritance -public expect abstract class AbstractBotConfiguration @MiraiInternalApi protected constructor() { +public abstract class AbstractBotConfiguration { // open for Java protected abstract var deviceInfo: ((Bot) -> DeviceInfo)? protected abstract var networkLoggerSupplier: ((Bot) -> MiraiLogger) protected abstract var botLoggerSupplier: ((Bot) -> MiraiLogger) + + /** + * 工作目录. 默认为 "." + */ + public var workingDir: File = File(".") + + /////////////////////////////////////////////////////////////////////////// + // Device + /////////////////////////////////////////////////////////////////////////// + /** * 使用文件存储设备信息. * * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常. - * @param filepath 文件路径. 默认是相对于 `workingDir` 的文件 "device.json". - * @see BotConfiguration.deviceInfo + * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json". + * @see deviceInfo */ @JvmOverloads @BotConfiguration.ConfigurationDsl - public fun fileBasedDeviceInfo(filepath: String = "device.json") + public fun fileBasedDeviceInfo(filepath: String = "device.json") { + deviceInfo = { + workingDir.resolve(filepath).loadAsDeviceInfo(BotConfiguration.json) + } + } + + /////////////////////////////////////////////////////////////////////////// + // Logging + /////////////////////////////////////////////////////////////////////////// + + + /** + * 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs]) + * 默认目录路径为 "$workingDir/logs/". + * @see DirectoryLogger + * @see redirectNetworkLogToDirectory + */ + @JvmOverloads + @BotConfiguration.ConfigurationDsl + public fun redirectNetworkLogToDirectory( + dir: File = File("logs"), + retain: Long = 1.weeksToMillis, + identity: (bot: Bot) -> String = { "Net ${it.id}" } + ) { + require(!dir.isFile) { "dir must not be a file" } + networkLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) } + } + + /** + * 重定向 [网络日志][networkLoggerSupplier] 到指定文件. 默认文件路径为 "$workingDir/mirai.log". + * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile]) + * @see SingleFileLogger + * @see redirectNetworkLogToDirectory + */ + @JvmOverloads + @BotConfiguration.ConfigurationDsl + public fun redirectNetworkLogToFile( + file: File = File("mirai.log"), + identity: (bot: Bot) -> String = { "Net ${it.id}" } + ) { + require(!file.isDirectory) { "file must not be a dir" } + networkLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) } + } + + /** + * 重定向 [Bot 日志][botLoggerSupplier] 到指定文件. + * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile]) + * @see SingleFileLogger + * @see redirectBotLogToDirectory + */ + @JvmOverloads + @BotConfiguration.ConfigurationDsl + public fun redirectBotLogToFile( + file: File = File("mirai.log"), + identity: (bot: Bot) -> String = { "Bot ${it.id}" } + ) { + require(!file.isDirectory) { "file must not be a dir" } + botLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) } + } + + + /** + * 重定向 [Bot 日志][botLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs]) + * @see DirectoryLogger + * @see redirectBotLogToFile + */ + @JvmOverloads + @BotConfiguration.ConfigurationDsl + public fun redirectBotLogToDirectory( + dir: File = File("logs"), + retain: Long = 1.weeksToMillis, + identity: (bot: Bot) -> String = { "Bot ${it.id}" } + ) { + require(!dir.isFile) { "dir must not be a file" } + botLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) } + } + + /////////////////////////////////////////////////////////////////////////// + // Cache + ////////////////////////////////////////////////////////////////////////// + + /** + * 缓存数据目录, 相对于 [workingDir]. + * + * 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行. + * 默认启用的缓存可以加快登录过程. + * + * 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含: + * - 联系人列表 + * - 登录服务器列表 + * - 资源服务秘钥 + * + * 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件. + * + * @since 2.4 + */ + public var cacheDir: File = File("cache") + + /////////////////////////////////////////////////////////////////////////// + // Misc + /////////////////////////////////////////////////////////////////////////// - internal fun applyMppCopy(new: BotConfiguration) -} \ No newline at end of file + internal fun applyMppCopy(new: BotConfiguration) { + new.workingDir = workingDir + new.cacheDir = cacheDir + } +} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/AbstractExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/AbstractExternalResource.kt similarity index 100% rename from mirai-core-api/src/jvmBaseMain/kotlin/utils/AbstractExternalResource.kt rename to mirai-core-api/src/commonMain/kotlin/utils/AbstractExternalResource.kt diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index fad49b70b2..383b43cc86 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -15,20 +15,27 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge +import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.contact.Contact.Companion.uploadImage +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.utils.* import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.message.data.Image +import net.mamoe.mirai.message.data.sendTo +import net.mamoe.mirai.message.data.toVoice import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.RandomAccessFile import kotlin.contracts.InvocationKind import kotlin.contracts.contract -import kotlin.jvm.JvmName -import kotlin.jvm.JvmOverloads -import kotlin.jvm.JvmStatic /** @@ -47,7 +54,7 @@ import kotlin.jvm.JvmStatic * ``` * file.toExternalResource().use { resource -> // 安全地使用资源 * contact.uploadImage(resource) // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件 + * contact.files.uploadNewFile("/foo/test.txt", resource) // 或者用来上传文件 * } * ``` * @@ -57,7 +64,7 @@ import kotlin.jvm.JvmStatic * inputStream.use { input -> // 安全地使用 InputStream * input.toExternalResource().use { resource -> // 安全地使用资源 * contact.uploadImage(resource) // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件 + * contact.files.uploadNewFile("/foo/test.txt", resource) // 或者用来上传文件 * } * } * ``` @@ -67,7 +74,7 @@ import kotlin.jvm.JvmStatic * ``` * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 + * contact.files.uploadNewFile("/foo/test.txt", resource); // 或者用来上传文件 * } * ``` * @@ -77,7 +84,7 @@ import kotlin.jvm.JvmStatic * try (InputStream stream = ...) { // 安全地使用 InputStream * try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源 * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 + * contact.files.uploadNewFile("/foo/test.txt", resource); // 或者用来上传文件 * } * } * ``` @@ -120,8 +127,7 @@ import kotlin.jvm.JvmStatic * * @see FileCacheStrategy */ -@Suppress("RemoveRedundantQualifierName") -public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { +public interface ExternalResource : java.io.Closeable { /** * 是否在 _使用一次_ 后自动 [close]. @@ -132,7 +138,8 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { * * @since 2.8 */ - public open val isAutoClose: Boolean + public val isAutoClose: Boolean + get() = false /** * 文件内容 MD5. 16 bytes @@ -143,7 +150,11 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { * 文件内容 SHA1. 16 bytes * @since 2.5 */ - public open val sha1: ByteArray + public val sha1: ByteArray + get() = + throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}") + // 如果你要实现 [ExternalResource], 你也应该实现 [sha1]. + // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现. /** @@ -168,8 +179,17 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { */ public val closed: Deferred + /** + * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流. + * + * 关闭此流不会关闭 [ExternalResource]. + * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出 + */ + public fun inputStream(): InputStream + /** * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流. + * 注意: 此 API 不稳定, 请使用 [inputStream] 代替. * * 关闭此流不会关闭 [ExternalResource]. * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出 @@ -180,7 +200,9 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { public fun input(): Input @MiraiInternalApi - public open fun calculateResourceId(): String + public fun calculateResourceId(): String { + return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME }) + } /** * 该 [ExternalResource] 的数据来源, 可能有以下的返回 @@ -201,14 +223,25 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { * * @since 2.8.0 */ - public open val origin: Any? + public val origin: Any? get() = null /** * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource]. * * @since 2.8.0 */ - public open fun toAutoCloseable(): ExternalResource + public fun toAutoCloseable(): ExternalResource { + return if (isAutoClose) this else { + val delegate = this + object : ExternalResource by delegate { + override val isAutoClose: Boolean get() = true + override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)" + override fun toAutoCloseable(): ExternalResource { + return this + } + } + } + } public companion object { @@ -217,13 +250,48 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { * * @see ExternalResource.formatName */ - @Suppress("CONST_VAL_WITHOUT_INITIALIZER") // compile bug - public const val DEFAULT_FORMAT_NAME: String + public const val DEFAULT_FORMAT_NAME: String = "mirai" /////////////////////////////////////////////////////////////////////////// // region toExternalResource /////////////////////////////////////////////////////////////////////////// + /** + * **打开文件**并创建 [ExternalResource]. + * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. + * + * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close]. + * + * @param formatName 查看 [ExternalResource.formatName] + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + public fun File.toExternalResource(formatName: String? = null): ExternalResource = + // although RandomAccessFile constructor throws IOException, performance influence is minor so not propagating IOException + RandomAccessFile(this, "r").toExternalResource(formatName).also { + it.cast().origin = this@toExternalResource + } + + /** + * 创建 [ExternalResource]. + * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭, 届时将会关闭 [RandomAccessFile]. + * + * **注意**:若关闭 [RandomAccessFile], 也会间接关闭 [ExternalResource]. + * + * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会. + * + * @param formatName 查看 [ExternalResource.formatName] + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + public fun RandomAccessFile.toExternalResource( + formatName: String? = null, + closeOriginalFileOnClose: Boolean = true, + ): ExternalResource = + ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose) + /** * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. * @@ -232,10 +300,67 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { @JvmStatic @JvmOverloads @JvmName("create") - public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource + public fun ByteArray.toExternalResource(formatName: String? = null): ExternalResource = + ExternalResourceImplByByteArray(this, formatName) + + + /** + * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource]. + * 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. + * + * **注意**:本函数不会关闭流. + * + * ### 在 Java 获得和使用 [ExternalResource] 实例 + * + * ``` + * try(ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file + * contact.uploadImage(resource); // 用来上传图片 + * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 + * } + * ``` + * + * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例: + * + * ``` + * try(InputStream stream = ...) { + * try(ExternalResource resource = ExternalResource.create(stream)) { + * contact.uploadImage(resource); // 用来上传图片 + * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 + * } + * } + * ``` + * + * + * @param formatName 查看 [ExternalResource.formatName] + * @see ExternalResource + */ + @JvmStatic + @JvmOverloads + @JvmName("create") + @Throws(IOException::class) // not in BIO context so propagate IOException + public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource = + Mirai.FileCacheStrategy.newCache(this, formatName) // endregion + + /* note: + 于 2.8.0-M1 添加 (#1392) + + 于 2.8.0-RC 移动至 `toExternalResource`(#1588) + */ + @JvmName("createAutoCloseable") + @JvmStatic + @Deprecated( + level = DeprecationLevel.HIDDEN, + message = "Moved to `toExternalResource()`", + replaceWith = ReplaceWith("resource.toAutoCloseable()"), + ) + @DeprecatedSinceMirai(errorSince = "2.8", hiddenSince = "2.10") + public fun createAutoCloseable(resource: ExternalResource): ExternalResource { + return resource.toAutoCloseable() + } + /////////////////////////////////////////////////////////////////////////// // region sendAsImageTo /////////////////////////////////////////////////////////////////////////// @@ -253,7 +378,43 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { @JvmBlockingBridge @JvmStatic @JvmName("sendAsImage") - public suspend fun ExternalResource.sendAsImageTo(contact: C): MessageReceipt + public suspend fun ExternalResource.sendAsImageTo(contact: C): MessageReceipt = + contact.uploadImage(this).sendTo(contact) + + /** + * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人. + * + * 注意:本函数不会关闭流. + * + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmBlockingBridge + @JvmName("sendAsImage") + @JvmOverloads + public suspend fun InputStream.sendAsImageTo( + contact: C, + formatName: String? = null, + ): MessageReceipt = + runBIO { + // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo + toExternalResource(formatName) + }.withUse { sendAsImageTo(contact) } + + /** + * 将文件作为图片发送到指定联系人. + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmBlockingBridge + @JvmName("sendAsImage") + @JvmOverloads + public suspend fun File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt { + require(this.exists() && this.canRead()) + return toExternalResource(formatName).withUse { sendAsImageTo(contact) } + } // endregion @@ -272,8 +433,196 @@ public expect interface ExternalResource : net.mamoe.mirai.utils.Closeable { */ @JvmStatic @JvmBlockingBridge - public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image + public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this) + /** + * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]. + * + * 注意:本函数不会关闭流. + * + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmBlockingBridge + @JvmOverloads + public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image = + // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo + runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) } + + // endregion + + /////////////////////////////////////////////////////////////////////////// + // region uploadAsFile + /////////////////////////////////////////////////////////////////////////// + + /** + * 将文件作为图片上传后构造 [Image]. + * + * @param formatName 查看 [ExternalResource.formatName] + * @throws OverFileSizeMaxException + */ + @JvmStatic + @JvmBlockingBridge + @JvmOverloads + public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image = + toExternalResource(formatName).withUse { uploadAsImage(contact) } + + /** + * 上传文件并获取文件消息. + * + * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. + * + * 需要调用方手动[关闭资源][ExternalResource.close]. + * + * ## 已弃用 + * 查看 [RemoteFile.upload] 获取更多信息. + * + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.upload + */ + @Suppress("DEPRECATION_ERROR") + @JvmStatic + @JvmBlockingBridge + @JvmOverloads + @Deprecated( + "Use sendTo instead.", + ReplaceWith( + "this.sendTo(contact, path, callback)", + "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo" + ), + level = DeprecationLevel.HIDDEN + ) // deprecated since 2.7-M1 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public suspend fun File.uploadTo( + contact: FileSupported, + path: String, + callback: RemoteFile.ProgressionCallback? = null, + ): FileMessage = toExternalResource().use { + contact.filesRoot.resolve(path).upload(it, callback) + } + + /** + * 上传文件并获取文件消息. + * + * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. + * + * 需要调用方手动[关闭资源][ExternalResource.close]. + * + * ## 已弃用 + * 查看 [RemoteFile.upload] 获取更多信息. + * + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.upload + */ + @Suppress("DEPRECATION_ERROR") + @JvmStatic + @JvmBlockingBridge + @JvmName("uploadAsFile") + @JvmOverloads + @Deprecated( + "Use sendAsFileTo instead.", + ReplaceWith( + "this.sendAsFileTo(contact, path, callback)", + "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo" + ), + level = DeprecationLevel.HIDDEN + ) // deprecated since 2.7-M1 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public suspend fun ExternalResource.uploadAsFile( + contact: FileSupported, + path: String, + callback: RemoteFile.ProgressionCallback? = null, + ): FileMessage { + return contact.filesRoot.resolve(path).upload(this, callback) + } + + // endregion + + /////////////////////////////////////////////////////////////////////////// + // region sendAsFileTo + /////////////////////////////////////////////////////////////////////////// + + /** + * 上传文件并发送文件消息. + * + * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. + * + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.uploadAndSend + */ + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Deprecated. Please use AbsoluteFolder.uploadNewFile", + ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), + level = DeprecationLevel.ERROR, + ) // deprecated since 2.8.0-RC + @JvmStatic + @JvmBlockingBridge + @JvmOverloads + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") + public suspend fun File.sendTo( + contact: C, + path: String, + callback: RemoteFile.ProgressionCallback? = null, + ): MessageReceipt = toExternalResource().use { + contact.filesRoot.resolve(path).upload(it, callback).sendTo(contact) + } + + /** + * 上传文件并发送件消息. 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. + * + * 需要调用方手动[关闭资源][ExternalResource.close]. + * + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.uploadAndSend + */ + @Suppress("DEPRECATION_ERROR") + @Deprecated( + "Deprecated. Please use AbsoluteFolder.uploadNewFile", + ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), + level = DeprecationLevel.ERROR + ) // deprecated since 2.8.0-RC + @JvmStatic + @JvmBlockingBridge + @JvmName("sendAsFile") + @JvmOverloads + @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") + public suspend fun ExternalResource.sendAsFileTo( + contact: C, + path: String, + callback: RemoteFile.ProgressionCallback? = null, + ): MessageReceipt { + return contact.filesRoot.resolve(path).upload(this, callback).sendTo(contact) + } + + // endregion + + /////////////////////////////////////////////////////////////////////////// + // region uploadAsVoice + /////////////////////////////////////////////////////////////////////////// + + @Suppress("DEPRECATION_ERROR") + @JvmBlockingBridge + @JvmStatic + @Deprecated( + "Use `contact.uploadAudio(resource)` instead", + level = DeprecationLevel.HIDDEN + ) // deprecated since 2.7 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice { + @Suppress("DEPRECATION_ERROR") + if (contact is Group) return contact.uploadAudio(this).toVoice() + else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice") + } // endregion } } diff --git a/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt b/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt index 355a9dc71f..50bf2a5e50 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/FileCacheStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -12,12 +12,16 @@ package net.mamoe.mirai.utils import io.ktor.utils.io.errors.* +import kotlinx.coroutines.Dispatchers import net.mamoe.mirai.Bot import net.mamoe.mirai.IMirai import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage -import kotlin.jvm.JvmStatic +import net.mamoe.mirai.utils.FileCacheStrategy.MemoryCache +import net.mamoe.mirai.utils.FileCacheStrategy.TempCache +import java.io.File +import java.io.InputStream /** * 资源缓存策略. @@ -48,7 +52,66 @@ import kotlin.jvm.JvmStatic * * @see ExternalResource */ -public expect interface FileCacheStrategy { +public interface FileCacheStrategy { + /** + * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. + * + * 注意: + * - 此函数不会关闭输入 + * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). + * + * @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai" + */ + @Throws(IOException::class) + public fun newCache(input: InputStream, formatName: String? = null): ExternalResource + + /** + * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai". + * + * 注意: + * - 此函数不会关闭输入 + * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). + */ + @Throws(IOException::class) + public fun newCache(input: InputStream): ExternalResource = newCache(input, null) + + /** + * 使用内存直接存储所有图片文件. 由 JVM 执行 GC. + */ + public object MemoryCache : FileCacheStrategy { + @Throws(IOException::class) + override fun newCache(input: InputStream, formatName: String?): ExternalResource { + return input.readBytes().toExternalResource(formatName) + } + } + + /** + * 使用系统临时文件夹缓存图片文件. 在图片使用完毕后或 JVM 正常结束时删除临时文件. + */ + public class TempCache @JvmOverloads public constructor( + /** + * 缓存图片存放位置. 为 `null` 时使用主机系统的临时文件夹: `File.createTempFile("tmp", null, directory)` + */ + public val directory: File? = null, + ) : FileCacheStrategy { + private fun createTempFile(): File { + return File.createTempFile("tmp", null, directory) + } + + @Throws(IOException::class) + override fun newCache(input: InputStream, formatName: String?): ExternalResource { + val file = createTempFile() + return file.apply { + deleteOnExit() + outputStream().use { out -> input.copyTo(out) } + }.toExternalResource(formatName).apply { + closed.invokeOnCompletion { + kotlin.runCatching { file.delete() } + } + } + } + } + public companion object { /** * 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai 从 [IMirai.FileCacheStrategy] 获取. @@ -57,6 +120,6 @@ public expect interface FileCacheStrategy { */ @MiraiExperimentalApi @JvmStatic - public val PlatformDefault: FileCacheStrategy + public val PlatformDefault: FileCacheStrategy = TempCache(null) } } diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/FileLogger.kt b/mirai-core-api/src/commonMain/kotlin/utils/FileLogger.kt similarity index 97% rename from mirai-core-api/src/jvmBaseMain/kotlin/utils/FileLogger.kt rename to mirai-core-api/src/commonMain/kotlin/utils/FileLogger.kt index 2f8bac196f..5ce5f6bdec 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/FileLogger.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/FileLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/LoggerAdapters.kt b/mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt similarity index 97% rename from mirai-core-api/src/jvmBaseMain/kotlin/utils/LoggerAdapters.kt rename to mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt index 4acea122dd..a6f8f00335 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/LoggerAdapters.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt b/mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt index dd1e3f8cb1..1fb61775d0 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt @@ -12,9 +12,8 @@ package net.mamoe.mirai.utils -import kotlin.jvm.JvmMultifileClass -import kotlin.jvm.JvmName -import kotlin.jvm.JvmOverloads +import me.him188.kotlin.dynamic.delegation.dynamicDelegation +import java.util.* import kotlin.reflect.KClass /** @@ -23,6 +22,7 @@ import kotlin.reflect.KClass @JvmOverloads public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitch = MiraiLoggerWithSwitch(this, default) + /** * 日志记录器. * @@ -30,6 +30,12 @@ public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitc * * Mirai 内建简单的日志系统, 即 [MiraiLogger]. [MiraiLogger] 的实现有 [SimpleLogger], [PlatformLogger], [SilentLogger]. * + * [MiraiLogger] 仅能处理简单的日志任务, 通常推荐使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 等日志库. + * + * ## 使用第三方日志库接管 Mirai 日志系统 + * + * 使用 [LoggerAdapters], 将第三方日志 `Logger` 转为 [MiraiLogger]. 然后通过 [MiraiLogger.Factory] 提供实现. + * * ## 实现或使用 [MiraiLogger] * * 不建议实现或使用 [MiraiLogger]. 请优先考虑使用上述第三方框架. [MiraiLogger] 仅应用于兼容旧版本代码. @@ -37,10 +43,11 @@ public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitc * @see SimpleLogger 简易 logger, 它将所有的日志记录操作都转移给 lambda `(String?, Throwable?) -> Unit` * @see PlatformLogger 各个平台下的默认日志记录实现. * @see SilentLogger 忽略任何日志记录操作的 logger 实例. + * @see LoggerAdapters * * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数. */ -public expect interface MiraiLogger { +public interface MiraiLogger { /** * 可以 service 实现的方式覆盖. @@ -54,19 +61,74 @@ public expect interface MiraiLogger { * @param requester 请求创建 [MiraiLogger] 的对象的 class * @param identity 对象标记 (备注) */ - public open fun create(requester: KClass<*>, identity: String? = null): MiraiLogger + public fun create(requester: KClass<*>, identity: String? = null): MiraiLogger = + this.create(requester.java, identity) + + /** + * 创建 [MiraiLogger] 实例. + * + * @param requester 请求创建 [MiraiLogger] 的对象的 class + * @param identity 对象标记 (备注) + */ + public fun create(requester: Class<*>, identity: String? = null): MiraiLogger + + /** + * 创建 [MiraiLogger] 实例. + * + * @param requester 请求创建 [MiraiLogger] 的对象 + */ + public fun create(requester: KClass<*>): MiraiLogger = create(requester, null) /** * 创建 [MiraiLogger] 实例. * * @param requester 请求创建 [MiraiLogger] 的对象 */ - public open fun create(requester: KClass<*>): MiraiLogger + public fun create(requester: Class<*>): MiraiLogger = create(requester, null) - public companion object INSTANCE : Factory + public companion object INSTANCE : + Factory by dynamicDelegation({ MiraiLoggerFactoryImplementationBridge }) } - public companion object; + public companion object { + /** + * 顶层日志, 仅供 Mirai 内部使用. + */ + @MiraiInternalApi + @MiraiExperimentalApi + @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") } + + /** + * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供. + */ + @Deprecated( + "Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory", + level = DeprecationLevel.HIDDEN + ) // deprecated since 2.7 + @JvmStatic + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.13") + public fun setDefaultLoggerCreator(@Suppress("UNUSED_PARAMETER") creator: (identity: String?) -> MiraiLogger) { + // nop + + +// DefaultFactoryOverrides.override { _, identity -> creator(identity) } + } + + /** + * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create]. + */ + @Deprecated( + "Please use MiraiLogger.Factory.create", ReplaceWith( + "MiraiLogger.Factory.create(YourClass::class, identity)", + "net.mamoe.mirai.utils.MiraiLogger" + ), level = DeprecationLevel.HIDDEN + ) // deprecated since 2.7 + @JvmStatic + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity) + } /** * 日志的标记. 在 Mirai 中, identity 可为 @@ -89,51 +151,63 @@ public expect interface MiraiLogger { * 当 VERBOSE 级别的日志启用时返回 `true`. * * 若 [isEnabled] 为 `false`, 返回 `false`. + * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. * 其他情况下返回 [isEnabled] 的值. * * @since 2.7 */ - public open val isVerboseEnabled: Boolean + public val isVerboseEnabled: Boolean get() = isEnabled /** * 当 DEBUG 级别的日志启用时返回 `true` * * 若 [isEnabled] 为 `false`, 返回 `false`. + * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. * 其他情况下返回 [isEnabled] 的值. * * @since 2.7 */ - public open val isDebugEnabled: Boolean + public val isDebugEnabled: Boolean get() = isEnabled /** * 当 INFO 级别的日志启用时返回 `true` * * 若 [isEnabled] 为 `false`, 返回 `false`. + * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. * 其他情况下返回 [isEnabled] 的值. * * @since 2.7 */ - public open val isInfoEnabled: Boolean + public val isInfoEnabled: Boolean get() = isEnabled /** * 当 WARNING 级别的日志启用时返回 `true` * * 若 [isEnabled] 为 `false`, 返回 `false`. + * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. * 其他情况下返回 [isEnabled] 的值. * * @since 2.7 */ - public open val isWarningEnabled: Boolean + public val isWarningEnabled: Boolean get() = isEnabled /** * 当 ERROR 级别的日志启用时返回 `true` * * 若 [isEnabled] 为 `false`, 返回 `false`. + * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. * 其他情况下返回 [isEnabled] 的值. * * @since 2.7 */ - public open val isErrorEnabled: Boolean + public val isErrorEnabled: Boolean get() = isEnabled + + @Suppress("UNUSED_PARAMETER") + @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public var follower: MiraiLogger? + get() = null + set(value) {} /** * 记录一个 `verbose` 级别的日志. @@ -141,7 +215,7 @@ public expect interface MiraiLogger { */ public fun verbose(message: String?) - public open fun verbose(e: Throwable?) + public fun verbose(e: Throwable?): Unit = verbose(null, e) public fun verbose(message: String?, e: Throwable?) /** @@ -149,7 +223,7 @@ public expect interface MiraiLogger { */ public fun debug(message: String?) - public open fun debug(e: Throwable?) + public fun debug(e: Throwable?): Unit = debug(null, e) public fun debug(message: String?, e: Throwable?) @@ -158,7 +232,7 @@ public expect interface MiraiLogger { */ public fun info(message: String?) - public open fun info(e: Throwable?) + public fun info(e: Throwable?): Unit = info(null, e) public fun info(message: String?, e: Throwable?) @@ -167,7 +241,7 @@ public expect interface MiraiLogger { */ public fun warning(message: String?) - public open fun warning(e: Throwable?) + public fun warning(e: Throwable?): Unit = warning(null, e) public fun warning(message: String?, e: Throwable?) @@ -176,13 +250,18 @@ public expect interface MiraiLogger { */ public fun error(message: String?) - public open fun error(e: Throwable?) + public fun error(e: Throwable?): Unit = error(null, e) public fun error(message: String?, e: Throwable?) /** 根据优先级调用对应函数 */ - public open fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null) -} + public fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null): Unit = + @OptIn(MiraiExperimentalApi::class) priority.correspondingFunction(this, message, e) + + @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 + @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") + public operator fun plus(follower: T): T = follower +} public inline fun MiraiLogger.verbose(message: () -> String) { if (isVerboseEnabled) verbose(message()) diff --git a/mirai-core-api/src/commonMain/kotlin/utils/MiraiLoggerFactoryImplementationBridge.kt b/mirai-core-api/src/commonMain/kotlin/utils/MiraiLoggerFactoryImplementationBridge.kt new file mode 100644 index 0000000000..178edd49f4 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/MiraiLoggerFactoryImplementationBridge.kt @@ -0,0 +1,94 @@ +/* + * 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.utils + +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.loop +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.reflect.KClass + +/** + * @since 2.13 + */ +internal object MiraiLoggerFactoryImplementationBridge : MiraiLogger.Factory { + private var _instance by lateinitMutableProperty { + createPlatformInstance() + } + + internal val instance get() = _instance + + // It is required for MiraiConsole because default implementation + // queries stdout on every message printing + // It creates an infinite loop (StackOverflowError) + internal var defaultLoggerFactory: (() -> MiraiLogger.Factory) = ::DefaultFactory + + fun createPlatformInstance() = loadService(MiraiLogger.Factory::class, defaultLoggerFactory) + + private val frozen = atomic(false) + + fun freeze(): Boolean { + return frozen.compareAndSet(expect = false, update = true) + } + + @TestOnly + fun reinit() { + defaultLoggerFactory = ::DefaultFactory + frozen.loop { value -> + _instance = createPlatformInstance() + if (frozen.compareAndSet(value, false)) return + } + } + + fun setInstance(instance: MiraiLogger.Factory) { + if (frozen.value) { + error( + "LoggerFactory instance had been frozen, so it's impossible to override it." + + "If you are using Mirai Console and you want to override platform logging implementation, " + + "please do so before initialization of MiraiConsole, that is, before `MiraiConsoleImplementation.start()`. " + + "Plugins are not allowed to override logging implementation, and this is done in the very fundamental implementation of Mirai Console so there is no way to escape that." + + "Normally it is only sensible for Mirai Console frontend implementor to do that." + + "If you are just using mirai-core, this error should not happen. There should be no limitation in overriding logging implementation with mirai-core. " + + "Check if you actually did use mirai-console somewhere, or please file an issue on https://github.com/mamoe/mirai/issues/new/choose" + ) + } + this._instance = instance + } + + inline fun wrapCurrent(mapper: (current: MiraiLogger.Factory) -> MiraiLogger.Factory) { + contract { callsInPlace(mapper, InvocationKind.EXACTLY_ONCE) } + setInstance(this.instance.let(mapper)) + } + + override fun create(requester: KClass<*>, identity: String?): MiraiLogger { + return instance.create(requester, identity) + } + + override fun create(requester: Class<*>, identity: String?): MiraiLogger { + return instance.create(requester, identity) + } + + override fun create(requester: KClass<*>): MiraiLogger { + return instance.create(requester) + } + + override fun create(requester: Class<*>): MiraiLogger { + return instance.create(requester) + } +} + + +// used by Mirai Console +private class DefaultFactory : MiraiLogger.Factory { + @OptIn(MiraiInternalApi::class) + override fun create(requester: Class<*>, identity: String?): MiraiLogger { + return PlatformLogger(identity ?: requester.kotlin.simpleName ?: requester.simpleName) + } +} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt similarity index 99% rename from mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt rename to mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt index 7462d37756..03315baf81 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/RemoteFile.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/SingleFileLogger.kt b/mirai-core-api/src/commonMain/kotlin/utils/SingleFileLogger.kt similarity index 89% rename from mirai-core-api/src/jvmBaseMain/kotlin/utils/SingleFileLogger.kt rename to mirai-core-api/src/commonMain/kotlin/utils/SingleFileLogger.kt index 8d32b145a8..b1d8342417 100644 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/SingleFileLogger.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/SingleFileLogger.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -7,8 +7,6 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -@file:JvmName("FileLoggerKt") // bin-comp - package net.mamoe.mirai.utils import java.io.File diff --git a/mirai-core-api/src/commonMain/kotlin/utils/Streamable.kt b/mirai-core-api/src/commonMain/kotlin/utils/Streamable.kt index f702e20878..bde715577f 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/Streamable.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/Streamable.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. @@ -11,31 +11,54 @@ package net.mamoe.mirai.utils +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge +import net.mamoe.mirai.contact.announcement.Announcement import net.mamoe.mirai.contact.announcement.Announcements -import net.mamoe.mirai.contact.announcement.OnlineAnnouncement +import net.mamoe.mirai.utils.JdkStreamSupport.toStream +import java.util.stream.Stream +import kotlin.coroutines.EmptyCoroutineContext /** - * 表示一个可以创建[数据流][Flow]的对象. + * 表示一个可以创建数据流 [Flow] 和 [Stream] 的对象. * * 实现这个接口的对象可以看做为元素 [T] 的集合. - * 例如 [Announcements] 可以看作是 [OnlineAnnouncement] 的集合, - * 使用 [Announcements.asFlow] 可以获取到包含所有 [OnlineAnnouncement] 列表的 [Flow] - * 在 JVM, 还可以使用 `Announcements.asStream` 可以获取到包含所有 [OnlineAnnouncement] 列表的 `Stream`. + * 例如 [Announcements] 可以看作是 [Announcement] 的集合, + * 使用 [Announcements.asFlow] 可以获取到包含所有 [Announcement] 列表的 [Flow], + * 使用 [Announcements.asStream] 可以获取到包含所有 [Announcement] 列表的 [Stream]. * * @since 2.13 */ -public expect interface Streamable { +public interface Streamable { /** * 创建一个能获取 [T] 的 [Flow]. */ public fun asFlow(): Flow + /** + * 创建一个能获取该群内所有 [T] 的 [Stream]. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [asFlow]. + * + * 注: 为了资源的正确释放, 使用 [Stream] 时需要使用 `try-with-resource`. 如 + * + * ```java + * Streamable tmp; + * try (var stream = tmp.asStream()) { + * System.out.println(stream.findFirst()); + * } + * ``` + */ + public fun asStream(): Stream = asFlow().toStream( + context = if (this is CoroutineScope) this.coroutineContext else EmptyCoroutineContext, + ) + /** * 获取所有 [T] 列表, 将全部 [T] 都加载后再返回. * * @return 此时刻的 [T] 只读列表. */ - public open suspend fun toList(): List + public suspend fun toList(): List = asFlow().toList() } \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/logging/AbstractLoggingTest.kt b/mirai-core-api/src/commonTest/kotlin/logging/AbstractLoggingTest.kt similarity index 91% rename from mirai-core-api/src/jvmBaseTest/kotlin/logging/AbstractLoggingTest.kt rename to mirai-core-api/src/commonTest/kotlin/logging/AbstractLoggingTest.kt index b9eb72a0c5..c7383ec597 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/logging/AbstractLoggingTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/logging/AbstractLoggingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/logging/Log4j2LoggingTest.kt b/mirai-core-api/src/commonTest/kotlin/logging/Log4j2LoggingTest.kt similarity index 98% rename from mirai-core-api/src/jvmBaseTest/kotlin/logging/Log4j2LoggingTest.kt rename to mirai-core-api/src/commonTest/kotlin/logging/Log4j2LoggingTest.kt index 4d9c6a5483..0d3d688b5a 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/logging/Log4j2LoggingTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/logging/Log4j2LoggingTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/logging/LoggingCompatibilityTest.kt b/mirai-core-api/src/commonTest/kotlin/logging/LoggingCompatibilityTest.kt similarity index 95% rename from mirai-core-api/src/jvmBaseTest/kotlin/logging/LoggingCompatibilityTest.kt rename to mirai-core-api/src/commonTest/kotlin/logging/LoggingCompatibilityTest.kt index aeeed68df4..cff4648de3 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/logging/LoggingCompatibilityTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/logging/LoggingCompatibilityTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/message/data/MessageChainImmutableTest.kt b/mirai-core-api/src/commonTest/kotlin/message.data/MessageChainImmutableTest.kt similarity index 95% rename from mirai-core-api/src/jvmBaseTest/kotlin/message/data/MessageChainImmutableTest.kt rename to mirai-core-api/src/commonTest/kotlin/message.data/MessageChainImmutableTest.kt index f1f86f53df..dc2b0f9384 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/message/data/MessageChainImmutableTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/message.data/MessageChainImmutableTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt b/mirai-core-api/src/commonTest/kotlin/utils/JvmDeviceInfoTest.kt similarity index 99% rename from mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt rename to mirai-core-api/src/commonTest/kotlin/utils/JvmDeviceInfoTest.kt index b73cccd1a0..f550664d89 100644 --- a/mirai-core-api/src/jvmBaseTest/kotlin/utils/JvmDeviceInfoTest.kt +++ b/mirai-core-api/src/commonTest/kotlin/utils/JvmDeviceInfoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Mamoe Technologies and contributors. + * 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. diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/Contact.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/Contact.kt deleted file mode 100644 index dcd2bad0a7..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/Contact.kt +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmBlockingBridge - -package net.mamoe.mirai.contact - -import kotlinx.coroutines.CoroutineScope -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.Bot -import net.mamoe.mirai.event.events.* -import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.message.data.isContentEmpty -import net.mamoe.mirai.message.data.toPlainText -import net.mamoe.mirai.utils.ExternalResource -import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo -import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage -import net.mamoe.mirai.utils.FileCacheStrategy -import net.mamoe.mirai.utils.NotStableForInheritance -import net.mamoe.mirai.utils.OverFileSizeMaxException -import java.io.File -import java.io.InputStream - -/** - * 联系对象, 即可以与 [Bot] 互动的对象. 包含 [用户][User], 和 [群][Group]. - */ -@NotStableForInheritance -public actual interface Contact : ContactOrBot, CoroutineScope { - /** - * 这个联系对象所属 [Bot]. - */ - public actual override val bot: Bot - - /** - * 可以是 QQ 号码或者群号码. - * - * @see User.id - * @see Group.id - */ - public actual override val id: Long - - /** - * 向这个对象发送消息. - * - * 单条消息最大可发送 4500 字符或 50 张图片. - * - * @see MessagePreSendEvent 发送消息前事件 - * @see MessagePostSendEvent 发送消息后事件 - * - * @throws EventCancelledException 当发送消息事件被取消时抛出 - * @throws BotIsBeingMutedException 发送群消息时若 [Bot] 被禁言抛出 - * @throws MessageTooLargeException 当消息过长时抛出 - * @throws IllegalArgumentException 当消息内容为空时抛出 (详见 [Message.isContentEmpty]) - * - * @return 消息回执. 可 [引用][MessageReceipt.quote] 或 [撤回][MessageReceipt.recall] 这条消息. - */ - public actual suspend fun sendMessage(message: Message): MessageReceipt - - /** - * 发送纯文本消息 - * @see sendMessage - */ - public actual suspend fun sendMessage(message: String): MessageReceipt = - this.sendMessage(message.toPlainText()) - - /** - * 上传一个 [资源][ExternalResource] 作为图片以备发送. - * - * **无论上传是否成功都不会关闭 [resource]. 需要调用方手动关闭资源** - * - * 也可以使用其他扩展: [ExternalResource.uploadAsImage] 使用 [File], [InputStream] 等上传. - * - * @see Image 查看有关图片的更多信息, 如上传图片 - * - * @see BeforeImageUploadEvent 图片发送前事件, 可拦截. - * @see ImageUploadEvent 图片发送完成事件, 不可拦截. - * - * @see ExternalResource - * - * @throws EventCancelledException 当发送消息事件被取消时抛出 - * @throws OverFileSizeMaxException 当图片文件过大而被服务器拒绝上传时抛出. (最大大小约为 20 MB, 但 mirai 限制的大小为 30 MB) - */ - public actual suspend fun uploadImage(resource: ExternalResource): Image - - public actual companion object { - /** - * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人 - * - * 注意:此函数不会关闭 [imageStream] - * - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - * @see FileCacheStrategy - */ - @JvmStatic - @JvmOverloads - public suspend fun C.sendImage( - imageStream: InputStream, - formatName: String? = null - ): MessageReceipt = imageStream.sendAsImageTo(this, formatName) - - /** - * 将文件作为图片发送到指定联系人 - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - * @see FileCacheStrategy - */ - @JvmStatic - @JvmOverloads - public suspend fun C.sendImage( - file: File, - formatName: String? = null - ): MessageReceipt = file.sendAsImageTo(this, formatName) - - /** - * 将资源作为单独的图片消息发送给 [this] - * - * @see Contact.sendMessage 最终调用, 发送消息. - */ - @JvmStatic - public actual suspend fun C.sendImage(resource: ExternalResource): MessageReceipt = - resource.sendAsImageTo(this) - - - /** - * 读取 [InputStream] 到临时文件并将其作为图片上传, 但不发送 - * - * 注意:本函数不会关闭流 - * - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmOverloads - public suspend fun Contact.uploadImage( - imageStream: InputStream, - formatName: String? = null - ): Image = imageStream.uploadAsImage(this@uploadImage, formatName) - - /** - * 将文件作为图片上传, 但不发送 - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmOverloads - public suspend fun Contact.uploadImage( - file: File, - formatName: String? = null - ): Image = file.uploadAsImage(this, formatName) - - /** - * 将文件作为图片上传, 但不发送 - * @throws OverFileSizeMaxException - */ - @Throws(OverFileSizeMaxException::class) - @JvmStatic - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "EXTENSION_SHADOWED_BY_MEMBER") - @kotlin.internal.LowPriorityInOverloadResolution // for better Java API - public actual suspend fun Contact.uploadImage(resource: ExternalResource): Image = this.uploadImage(resource) - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt deleted file mode 100644 index 93a9f386b7..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/FileSupported.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2019-2022 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.contact - -import net.mamoe.mirai.contact.file.RemoteFiles -import net.mamoe.mirai.utils.DeprecatedSinceMirai -import net.mamoe.mirai.utils.NotStableForInheritance - - -/** - * 支持文件操作的 [Contact]. 目前仅 [Group]. - * - * 获取文件操作相关示例: [RemoteFiles] - * - * @since 2.5 - * - * @see RemoteFiles - */ -@NotStableForInheritance -public actual interface FileSupported : Contact { - /** - * 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表. - * - * **注意:** 已弃用, 请使用 [files]. - * - * @since 2.5 - */ - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - @Deprecated( - "Please use files instead.", - replaceWith = ReplaceWith("files.root"), - level = DeprecationLevel.ERROR - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") - public val filesRoot: net.mamoe.mirai.utils.RemoteFile - - /** - * 获取远程文件列表 (管理器). - * - * @since 2.8 - */ - public actual val files: RemoteFiles -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt deleted file mode 100644 index 1c214bcebd..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/file/AbsoluteFolder.kt +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmBlockingBridge - -package net.mamoe.mirai.contact.file - -import kotlinx.coroutines.flow.Flow -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.contact.PermissionDeniedException -import net.mamoe.mirai.utils.ExternalResource -import net.mamoe.mirai.utils.JavaFriendlyAPI -import net.mamoe.mirai.utils.NotStableForInheritance -import net.mamoe.mirai.utils.ProgressionCallback -import java.util.stream.Stream - -/** - * 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响. - * - * @since 2.8 - * @see RemoteFiles - * @see AbsoluteFile - * @see AbsoluteFileFolder - */ -@Suppress("SEALED_INHERITOR_IN_DIFFERENT_MODULE") -@NotStableForInheritance -public actual interface AbsoluteFolder : AbsoluteFileFolder { - /** - * 当前快照中文件数量, 当有文件更新时(上传/删除文件) 该属性不会更新. - * - * 只可能通过 [refresh] 手动刷新 - * - * 特别的, 若该目录表示根目录, [contentsCount] 返回 `0`. (无法快速获取) - */ - public actual val contentsCount: Int - - /** - * 当该目录为空时返回 `true`. - */ - public actual fun isEmpty(): Boolean = contentsCount == 0 - - /** - * 返回更新了文件或目录信息 ([lastModifiedTime] 等) 的, 指向相同文件的 [AbsoluteFileFolder]. - * 不会更新当前 [AbsoluteFileFolder] 对象. - * - * 当远程文件或目录不存在时返回 `null`. - * - * 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用. - */ - actual override suspend fun refreshed(): AbsoluteFolder? - - /////////////////////////////////////////////////////////////////////////// - // list children - /////////////////////////////////////////////////////////////////////////// - - /** - * 获取该目录下所有子目录列表. - */ - public actual suspend fun folders(): Flow - - /** - * 获取该目录下所有子目录列表. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [folders]. - */ - @JavaFriendlyAPI - public suspend fun foldersStream(): Stream - - - /** - * 获取该目录下所有文件列表. - */ - public actual suspend fun files(): Flow - - /** - * 获取该目录下所有文件列表. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [files]. - */ - @JavaFriendlyAPI - public suspend fun filesStream(): Stream - - - /** - * 获取该目录下所有文件和子目录列表. - */ - public actual suspend fun children(): Flow - - /** - * 获取该目录下所有文件和子目录列表. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [children]. - */ - @JavaFriendlyAPI - public suspend fun childrenStream(): Stream - - /////////////////////////////////////////////////////////////////////////// - // resolve and upload - /////////////////////////////////////////////////////////////////////////// - - /** - * 创建一个名称为 [name] 的子目录. 返回成功创建的或已有的子目录. 当目标目录已经存在时则直接返回该目录. - * - * @throws IllegalArgumentException 当 [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出 - * @throws PermissionDeniedException 当权限不足时抛出 - */ - public actual suspend fun createFolder(name: String): AbsoluteFolder - - /** - * 获取一个已存在的名称为 [name] 的子目录. 当该名称的子目录不存在时返回 `null`. - * - * @throws IllegalArgumentException 当 [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出 - */ - public actual suspend fun resolveFolder(name: String): AbsoluteFolder? - - /** - * 获取一个已存在的 [AbsoluteFileFolder.id] 为 [id] 的子目录. 当该名称的子目录不存在时返回 `null`. - * - * @throws IllegalArgumentException 当 [id] 为空或无效时抛出 - * - * @since 2.9.0 - */ - public actual suspend fun resolveFolderById(id: String): AbsoluteFolder? - - /** - * 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找. - */ - @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI - @JvmOverloads - public actual suspend fun resolveFileById( - id: String, - deep: Boolean = false - ): AbsoluteFile? - - /** - * 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件. - */ - public actual suspend fun resolveFiles( - path: String - ): Flow - - /** - * 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveFiles]. - */ - @JavaFriendlyAPI - public suspend fun resolveFilesStream( - path: String - ): Stream - - /** - * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. - */ - public actual suspend fun resolveAll( - path: String - ): Flow - - /** - * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveAll]. - */ - @JavaFriendlyAPI - public suspend fun resolveAllStream( - path: String - ): Stream - - /** - * 上传一个文件到该目录, 返回上传成功的文件标识. - * - * 会在必要时尝试创建远程目录. - * - * ### [filepath] - * - * - 可以是 `foo.txt` 表示该目录下的文件 "foo.txt" - * - 也可以是 `sub/foo.txt` 表示该目录的子目录 "sub" 下的文件 "foo.txt". - * - 或是绝对路径 `/sub/foo.txt` 表示根目录的 "sub" 目录下的文件 "foo.txt" - * - * @param filepath 目标文件名 - * @param content 文件内容 - * @param callback 下载进度回调, 传递的 `progression` 是已下载字节数. - * - * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) - */ - @Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI - @JvmOverloads - public actual suspend fun uploadNewFile( - filepath: String, - content: ExternalResource, - callback: ProgressionCallback? = null, - ): AbsoluteFile - - public actual companion object { - /** - * 根目录 folder ID. - * @see id - */ - public actual const val ROOT_FOLDER_ID: String = "/" - } -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/package.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/package.kt deleted file mode 100644 index d2e1fcb874..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/package.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright 2019-2022 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.contact - diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt b/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt deleted file mode 100644 index bb77500596..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/contact/roaming/RoamingMessages.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmBlockingBridge - -package net.mamoe.mirai.contact.roaming - -import kotlinx.coroutines.flow.Flow -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.message.data.MessageChain -import net.mamoe.mirai.message.data.MessageSource -import net.mamoe.mirai.utils.JavaFriendlyAPI -import net.mamoe.mirai.utils.JdkStreamSupport.toStream -import java.util.stream.Stream - - -/** - * 漫游消息记录管理器. 可通过 [RoamingSupported.roamingMessages] 获得. - * - * @since 2.8 - * @see RoamingSupported - */ -public actual interface RoamingMessages { - /////////////////////////////////////////////////////////////////////////// - // Get list - /////////////////////////////////////////////////////////////////////////// - - /** - * 查询指定时间段内的漫游消息记录. Java Stream 方法查看 [getMessagesStream]. - * - * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. - * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. - * - * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. - * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.source`), 判断 [MessageSource.fromId] (发送人). - * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). - * - * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. - * - * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. - * - * @param timeStart 起始时间戳, 单位为秒. 可以为 `0`, 即表示从可以获取的最早的消息起. 负数将会被看是 `0`. - * @param timeEnd 结束时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值. - * @param filter 过滤器. - */ - @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI - public actual suspend fun getMessagesIn( - timeStart: Long, - timeEnd: Long, - filter: RoamingMessageFilter? = null - ): Flow - - /** - * 查询所有漫游消息记录. Java Stream 方法查看 [getAllMessagesStream]. - * - * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. - * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. - * - * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. - * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.source`), 判断 [MessageSource.fromId] (发送人). - * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). - * - * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. - * - * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. - * - * @param filter 过滤器. - */ - @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI - public actual suspend fun getAllMessages( - filter: RoamingMessageFilter? = null - ): Flow = getMessagesIn(0, Long.MAX_VALUE, filter) - - /** - * 查询指定时间段内的漫游消息记录. Kotlin Flow 版本查看 [getMessagesIn]. - * - * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. - * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. - * - * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. - * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.get(MessageSource.Key)`), 判断 [MessageSource.fromId] (发送人). - * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). - * - * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. - * - * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. - * - * @param timeStart 起始时间戳, 单位为秒. 可以为 `0`, 即表示从可以获取的最早的消息起. 负数将会被看是 `0`. - * @param timeEnd 结束时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值. - * @param filter 过滤器. - */ - @Suppress("OVERLOADS_INTERFACE") - @JvmOverloads - @JavaFriendlyAPI - public suspend fun getMessagesStream( - timeStart: Long, - timeEnd: Long, - filter: RoamingMessageFilter? = null - ): Stream = getMessagesIn(timeStart, timeEnd, filter).toStream() - - /** - * 查询所有漫游消息记录. Kotlin Flow 版本查看 [getAllMessages]. - * - * 返回查询到的漫游消息记录, 顺序为由新到旧. 这些 [MessageChain] 与从事件中收到的消息链相似, 属于在线消息. - * 可从 [MessageChain] 获取 [MessageSource] 来确定发送人等相关信息, 也可以进行引用回复或撤回. - * - * 注意, 返回的消息记录既包含机器人发送给目标用户的消息, 也包含目标用户发送给机器人的消息. - * 可通过 [MessageChain] 获取 [MessageSource] (用法为 `messageChain.get(MessageSource.Key)`), 判断 [MessageSource.fromId] (发送人). - * 消息的其他*元数据*信息也要通过 [MessageSource] 获取 (如 [MessageSource.time] 获取时间). - * - * 若只需要获取单向消息 (机器人发送给目标用户的消息或反之), 可使用 [RoamingMessageFilter.SENT] 或 [RoamingMessageFilter.RECEIVED] 作为 [filter] 参数传递. - * - * 性能提示: 请在 [filter] 执行筛选, 若 [filter] 返回 `false` 则不会解析消息链, 这对本函数的处理速度有决定性影响. - * - * @param filter 过滤器. - */ - @Suppress("OVERLOADS_INTERFACE") - @JvmOverloads - @JavaFriendlyAPI - public suspend fun getAllMessagesStream( - filter: RoamingMessageFilter? = null - ): Stream = getMessagesStream(0, Long.MAX_VALUE, filter) -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt deleted file mode 100644 index 9aa166c31d..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/EventChannel.kt +++ /dev/null @@ -1,736 +0,0 @@ -/* - * 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.event - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.sync.Mutex -import net.mamoe.mirai.Bot -import net.mamoe.mirai.IMirai -import net.mamoe.mirai.event.ConcurrencyKind.CONCURRENT -import net.mamoe.mirai.event.events.BotEvent -import net.mamoe.mirai.internal.event.registerEventHandler -import net.mamoe.mirai.utils.* -import java.util.function.Consumer -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -import kotlin.reflect.KClass - -/** - * 事件通道. - * - * 事件通道是监听事件的入口, 但不负责广播事件. 要广播事件, 使用 [Event.broadcast] 或 [IMirai.broadcastEvent]. - * - * ## 获取事件通道 - * - * [EventChannel] 不可自行构造, 只能通过 [GlobalEventChannel], [BotEvent], 或基于一个通道的过滤等操作获得. - * - * ### 全局事件通道 - * - * [GlobalEventChannel] 是单例对象, 表示全局事件通道, 可以获取到在其中广播的所有事件. - * - * ### [BotEvent] 事件通道 - * - * 若只需要监听某个 [Bot] 的事件, 可通过 [Bot.eventChannel] 获取到这样的 [EventChannel]. - * - * ## 通道操作 - * - * ### 对通道的操作 - * - 过滤通道: 通过 [EventChannel.filter]. 例如 `filter { it is BotEvent }` 得到一个只能监听到 [BotEvent] 的事件通道. - * - 转换为 Kotlin 协程 [Channel]: [EventChannel.forwardToChannel] - * - 添加 [CoroutineContext]: [context], [parentJob], [parentScope], [exceptionHandler] - * - * ### 创建事件监听 - * - [EventChannel.subscribe] 创建带条件的一个事件监听器. - * - [EventChannel.subscribeAlways] 创建一个总是监听事件的事件监听器. - * - [EventChannel.subscribeOnce] 创建一个只监听单次的事件监听器. - * - * ### 监听器生命周期 - * - * 阅读 [EventChannel.subscribe] 以获取监听器生命周期相关信息. - * - * ## 与 kotlinx-coroutines 交互 - * - * mirai [EventChannel] 设计比 kotlinx-coroutines 的 [Flow] 稳定版更早. - * [EventChannel] 的功能与 [Flow] 类似, 不过 [EventChannel] 在 [subscribe] (类似 [Flow.collect]) 时有优先级判定, 也允许[拦截][Event.intercept]. - * - * ### 通过 [Flow] 接收事件 - * - * 使用 [EventChannel.asFlow] 获得 [Flow], 然后可使用 [Flow.collect] 等操作. - * - * ### 转发事件到 [SendChannel] - * - * 使用 [EventChannel.forwardToChannel] 可将事件转发到指定 [SendChannel]. - */ -@NotStableForInheritance // since 2.12, before it was `final class`. -public actual abstract class EventChannel @MiraiInternalApi public actual constructor( - public actual val baseEventClass: KClass, - /** - * 此事件通道的默认 [CoroutineScope.coroutineContext]. 将会被添加给所有注册的事件监听器. - */ - public actual val defaultCoroutineContext: CoroutineContext, -) { - /** - * 创建事件监听并将监听结果转发到 [channel]. 当 [Channel.send] 抛出 [ClosedSendChannelException] 时停止 [Listener] 监听和转发. - * - * 返回创建的会转发监听到的所有事件到 [channel] 的[事件监听器][Listener]. [停止][Listener.complete] 该监听器会停止转发, 不会影响目标 [channel]. - * - * 若 [Channel.send] 挂起, 则监听器也会挂起, 也就可能会导致事件广播过程挂起. - * - * 示例: - * - * ``` - * val eventChannel: EventChannel = ... - * val channel = Channel() // kotlinx.coroutines.channels.Channel - * eventChannel.forwardToChannel(channel, priority = ...) - * - * // 其他地方 - * val event: BotEvent = channel.receive() // 挂起并接收一个事件 - * ``` - * - * @see subscribeAlways - * @see Channel - * @since 2.10 - */ - public actual fun forwardToChannel( - channel: SendChannel<@UnsafeVariance BaseEvent>, - coroutineContext: CoroutineContext, - priority: EventPriority, - ): Listener<@UnsafeVariance BaseEvent> { - return subscribe(baseEventClass, coroutineContext, priority = priority) { - try { - channel.send(it) - ListeningStatus.LISTENING - } catch (_: ClosedSendChannelException) { - ListeningStatus.STOPPED - } - } - } - - /** - * 通过 [Flow] 接收此通道内的所有事件. - * - * ``` - * val eventChannel: EventChannel = ... - * val flow: Flow = eventChannel.asFlow() - * - * flow.collect { // it - * // - * } - * - * flow.filterIsInstance.collect { // it: GroupMessageEvent - * // 处理事件 ... - * } - * - * flow.filterIsInstance.collect { // it: FriendMessageEvent - * // 处理事件 ... - * } - * ``` - * - * 类似于 [SharedFlow], [EventChannel.asFlow] 返回的 [Flow] 永远都不会停止. 因此上述示例 [Flow.collect] 永远都不会正常 (以抛出异常之外的) 结束. - * - * 通过 [asFlow] 接收事件相当于通过 [subscribeAlways] 以 [EventPriority.MONITOR] 监听事件. - * - * **注意**: [context], [parentJob] 等控制 [EventChannel.defaultCoroutineContext] 的操作对 [asFlow] 无效. 因为 [asFlow] 并不创建协程. - * - * @see Flow - * @since 2.12 - */ - public actual abstract fun asFlow(): Flow - - // region transforming operations - - /** - * 添加一个过滤器. 过滤器将在收到任何事件之后, 传递给通过 [EventChannel.subscribe] 注册的监听器之前调用. - * - * 若 [filter] 返回 `true`, 该事件将会被传给监听器. 否则将会被忽略, **监听器继续监听**. - * - * ## 线性顺序 - * 多个 [filter] 的处理是线性且有顺序的. 若一个 [filter] 已经返回了 `false` (代表忽略这个事件), 则会立即忽略, 而不会传递给后续过滤器. - * - * 示例: - * ``` - * GlobalEventChannel // GlobalEventChannel 会收到全局所有事件, 事件类型是 Event - * .filterIsInstance() // 过滤, 只接受 BotEvent - * .filter { event: BotEvent -> - * // 此时的 event 一定是 BotEvent - * event.bot.id == 123456 // 再过滤 event 的 bot.id - * } - * .subscribeAlways { event: BotEvent -> - * // 现在 event 是 BotEvent, 且 bot.id == 123456 - * } - * ``` - * - * ## 过滤器挂起 - * [filter] 允许挂起协程. **过滤器的挂起将被认为是事件监听器的挂起**. - * - * 过滤器挂起是否会影响事件处理, - * 取决于 [subscribe] 时的 [ConcurrencyKind] 和 [EventPriority]. - * - * ## 过滤器异常处理 - * 若 [filter] 抛出异常, 将被包装为 [ExceptionInEventChannelFilterException] 并重新抛出. - * - * @see filterIsInstance 过滤指定类型的事件 - */ - @JvmSynthetic - public actual fun filter(filter: suspend (event: BaseEvent) -> Boolean): EventChannel { - return FilterEventChannel(this, filter) - } - - /** - * [EventChannel.filter] 的 Java 版本. - * - * 添加一个过滤器. 过滤器将在收到任何事件之后, 传递给通过 [EventChannel.subscribe] 注册的监听器之前调用. - * - * 若 [filter] 返回 `true`, 该事件将会被传给监听器. 否则将会被忽略, **监听器继续监听**. - * - * ## 线性顺序 - * 多个 [filter] 的处理是线性且有顺序的. 若一个 [filter] 已经返回了 `false` (代表忽略这个事件), 则会立即忽略, 而不会传递给后续过滤器. - * - * 示例: - * ``` - * GlobalEventChannel // GlobalEventChannel 会收到全局所有事件, 事件类型是 Event - * .filterIsInstance(BotEvent.class) // 过滤, 只接受 BotEvent - * .filter(event -> - * // 此时的 event 一定是 BotEvent - * event.bot.id == 123456 // 再过滤 event 的 bot.id - * ) - * .subscribeAlways(event -> { - * // 现在 event 是 BotEvent, 且 bot.id == 123456 - * }) - * ``` - * - * ## 过滤器阻塞 - * [filter] 允许阻塞线程. **过滤器的阻塞将被认为是事件监听器的阻塞**. - * - * 过滤器阻塞是否会影响事件处理, - * 取决于 [subscribe] 时的 [ConcurrencyKind] 和 [EventPriority]. - * - * ## 过滤器异常处理 - * 若 [filter] 抛出异常, 将被包装为 [ExceptionInEventChannelFilterException] 并重新抛出. - * - * @see filterIsInstance 过滤指定类型的事件 - * - * @since 2.2 - */ - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public actual fun filter(filter: (event: BaseEvent) -> Boolean): EventChannel { - return filter { runBIO { filter(it) } } - } - - /** - * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] - * @see filter 获取更多信息 - */ - @JvmSynthetic - public actual inline fun filterIsInstance(): EventChannel = - filterIsInstance(E::class) - - /** - * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] - * @see filter 获取更多信息 - */ - public actual fun filterIsInstance(kClass: KClass): EventChannel { - return filter { kClass.isInstance(it) }.cast() - } - - /** - * 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel] - * @see filter 获取更多信息 - */ - public fun filterIsInstance(clazz: Class): EventChannel = - filterIsInstance(clazz.kotlin) - - - /** - * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineContexts]. - * [coroutineContexts] 会覆盖 [defaultCoroutineContext] 中的重复元素. - * - * 此操作不会修改 [`this.coroutineContext`][defaultCoroutineContext], 只会创建一个新的 [EventChannel]. - */ - public actual abstract fun context(vararg coroutineContexts: CoroutineContext): EventChannel - - /** - * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [this.coroutineContext][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] - * @see context - */ - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public actual fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel { - return context(coroutineExceptionHandler) - } - - /** - * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] - * @see context - */ - public actual fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel { - return context(CoroutineExceptionHandler { _, throwable -> - coroutineExceptionHandler(throwable) - }) - } - - /** - * 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler] - * @see context - * @since 2.12 - */ - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public fun exceptionHandler(coroutineExceptionHandler: Consumer): EventChannel { - return exceptionHandler { coroutineExceptionHandler.accept(it) } - } - - /** - * 将 [coroutineScope] 作为这个 [EventChannel] 的父作用域. - * - * 实际作用为创建一个新的 [EventChannel], - * 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [CoroutineScope.coroutineContext], - * 并以 [CoroutineScope] 中 [Job] (如果有) [作为父 Job][parentJob] - * - * @see parentJob - * @see context - * - * @see CoroutineScope.globalEventChannel `GlobalEventChannel.parentScope()` 的扩展 - */ - public actual fun parentScope(coroutineScope: CoroutineScope): EventChannel { - return context(coroutineScope.coroutineContext) - } - - /** - * 指定协程父 [Job]. 之后在此 [EventChannel] 下创建的事件监听器都会成为 [job] 的子任务, 当 [job] 被取消时, 所有的事件监听器都会被取消. - * - * 注意: 监听器不会失败 ([Job.cancel]). 监听器处理过程的异常都会被捕获然后交由 [CoroutineExceptionHandler] 处理, 因此 [job] 不会因为子任务监听器的失败而被取消. - * - * @see parentScope - * @see context - */ - public actual fun parentJob(job: Job): EventChannel { - return context(job) - } - - // endregion - - // region subscribe - - /** - * 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件. - * - * 每当 [事件广播][Event.broadcast] 时, [handler] 都会被执行. - * - * - * ## 创建监听 - * 调用本函数: - * ``` - * eventChannel.subscribe { /* 会收到此通道中的所有是 E 的事件 */ } - * ``` - * - * ## 生命周期 - * - * ### 通过协程作用域管理监听器 - * 本函数将会创建一个 [Job], 成为 [parentJob] 中的子任务. 可创建一个 [CoroutineScope] 来管理所有的监听器: - * ``` - * val scope = CoroutineScope(SupervisorJob()) - * - * val scopedChannel = eventChannel.parentScope(scope) // 将协程作用域 scope 附加到这个 EventChannel - * - * scopedChannel.subscribeAlways { /* ... */ } // 启动监听, 监听器协程会作为 scope 的子任务 - * scopedChannel.subscribeAlways { /* ... */ } // 启动监听, 监听器协程会作为 scope 的子任务 - * - * scope.cancel() // 停止了协程作用域, 也就取消了两个监听器 - * ``` - * - * 这个函数返回 [Listener], 它是一个 [CompletableJob]. 它会成为 [parentJob] 或 [parentScope] 的一个 [子任务][Job] - * - * ### 停止监听 - * 如果 [handler] 返回 [ListeningStatus.STOPPED] 监听器将被停止. - * - * 也可以通过 [subscribe] 返回值 [Listener] 的 [Listener.complete] - * - * ## 监听器调度 - * 监听器会被创建一个协程任务, 语义上在 [parentScope] 下运行. - * 通过 Kotlin [默认协程调度器][Dispatchers.Default] 在固定的全局共享线程池里执行, 除非有 [coroutineContext] 指定. - * - * 默认在 [handler] 中不能处理阻塞任务. 阻塞任务将会阻塞一个 Kotlin 全局协程调度线程并可能导致严重问题. - * 请通过 `withContext(Dispatchers.IO) { }` 等方法执行阻塞工作. - * - * ## 异常处理 - * - * **监听过程抛出的异常是需要尽可能避免的, 因为这将产生不确定性.** - * - * 当参数 [handler] 处理事件抛出异常时, 只会从监听方协程上下文 ([CoroutineContext]) 寻找 [CoroutineExceptionHandler] 处理异常, 即如下顺序: - * 1. 本函数参数 [coroutineContext] - * 2. [EventChannel.defaultCoroutineContext] - * 3. 若以上步骤无法获取 [CoroutineExceptionHandler], 则只会在日志记录异常. - * 因此建议先指定 [CoroutineExceptionHandler] (可通过 [EventChannel.exceptionHandler]) 再监听事件, 或者在监听事件中捕获异常. - * - * 因此, 广播方 ([Event.broadcast]) 不会知晓监听方产生的异常, 其 [Event.broadcast] 过程也不会因监听方产生异常而提前结束. - * - * ***备注***: 在 2.11 以前, 发生上述异常时还会从广播方和有关 [Bot] 协程域获取 [CoroutineExceptionHandler]. 因此行为不稳定而在 2.11 变更为上述过程. - * - * 事件处理时抛出异常不会停止监听器. - * - * 建议在事件处理中 (即 [handler] 里) 处理异常, - * 或在参数 [coroutineContext] 中添加 [CoroutineExceptionHandler], 或通过 [EventChannel.exceptionHandler]. - * - * ## 并发安全性 - * 基于 [concurrency] 参数, 事件监听器可以被允许并行执行. - * - * - 若 [concurrency] 为 [ConcurrencyKind.CONCURRENT], [handler] 可能被并行调用, 需要保证并发安全. - * - 若 [concurrency] 为 [ConcurrencyKind.LOCKED], [handler] 会被 [Mutex] 限制, 串行异步执行. - * - * ## 衍生监听方法 - * - * 这些方法仅 Kotlin 可用. - * - * - [syncFromEvent]: 挂起当前协程, 监听一个事件, 并尝试从这个事件中**获取**一个值 - * - [nextEvent]: 挂起当前协程, 直到监听到特定类型事件的广播并通过过滤器, 返回这个事件实例. - * - * @param coroutineContext 在 [defaultCoroutineContext] 的基础上, 给事件监听协程的额外的 [CoroutineContext]. - * @param concurrency 并发类型. 查看 [ConcurrencyKind] - * @param priority 监听优先级,优先级越高越先执行 - * @param handler 事件处理器. 在接收到事件时会调用这个处理器. 其返回值意义参考 [ListeningStatus]. 其异常处理参考上文 - * - * @return 监听器实例. 此监听器已经注册到指定事件上, 在事件广播时将会调用 [handler] - * - * - * @see selectMessages 以 `when` 的语法 '选择' 即将到来的一条消息. - * @see whileSelectMessages 以 `when` 的语法 '选择' 即将到来的所有消息, 直到不满足筛选结果. - * - * @see subscribeAlways 一直监听 - * @see subscribeOnce 只监听一次 - * - * @see subscribeMessages 监听消息 DSL - */ - @JvmSynthetic - public actual inline fun subscribe( - coroutineContext: CoroutineContext, - concurrency: ConcurrencyKind, - priority: EventPriority, - noinline handler: suspend E.(E) -> ListeningStatus, - ): Listener = subscribe(E::class, coroutineContext, concurrency, priority, handler) - - /** - * 与 [subscribe] 的区别是接受 [eventClass] 参数, 而不使用 `reified` 泛型. 通常推荐使用具体化类型参数. - * - * @return 监听器实例. 此监听器已经注册到指定事件上, 在事件广播时将会调用 [handler] - * @see subscribe - */ - @JvmSynthetic - public actual fun subscribe( - eventClass: KClass, - coroutineContext: CoroutineContext, - concurrency: ConcurrencyKind, - priority: EventPriority, - handler: suspend E.(E) -> ListeningStatus, - ): Listener = subscribeInternal( - eventClass, - createListener0(coroutineContext, concurrency, priority) { it.handler(it); } - ) - - /** - * 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件. - * 每当 [事件广播][Event.broadcast] 时, [handler] 都会被执行. - * - * 可在任意时候通过 [Listener.complete] 来主动停止监听. - * - * @param concurrency 并发类型默认为 [CONCURRENT] - * @param coroutineContext 在 [defaultCoroutineContext] 的基础上, 给事件监听协程的额外的 [CoroutineContext] - * @param priority 处理优先级, 优先级高的先执行 - * - * @return 监听器实例. 此监听器已经注册到指定事件上, 在事件广播时将会调用 [handler] - * - * @see subscribe 获取更多说明 - */ - @JvmSynthetic - public actual inline fun subscribeAlways( - coroutineContext: CoroutineContext, - concurrency: ConcurrencyKind, - priority: EventPriority, - noinline handler: suspend E.(E) -> Unit, - ): Listener = subscribeAlways(E::class, coroutineContext, concurrency, priority, handler) - - - /** - * @see subscribe - * @see subscribeAlways - */ - @JvmSynthetic - public actual fun subscribeAlways( - eventClass: KClass, - coroutineContext: CoroutineContext, - concurrency: ConcurrencyKind, - priority: EventPriority, - handler: suspend E.(E) -> Unit, - ): Listener = subscribeInternal( - eventClass, - createListener0(coroutineContext, concurrency, priority) { it.handler(it); ListeningStatus.LISTENING } - ) - - /** - * 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件, 只监听一次. - * 当 [事件广播][Event.broadcast] 时, [handler] 会被执行. - * - * 可在任意时候通过 [Listener.complete] 来主动停止监听. - * - * @param coroutineContext 在 [defaultCoroutineContext] 的基础上, 给事件监听协程的额外的 [CoroutineContext] - * @param priority 处理优先级, 优先级高的先执行 - * - * @see subscribe 获取更多说明 - */ - @JvmSynthetic - public actual inline fun subscribeOnce( - coroutineContext: CoroutineContext, - priority: EventPriority, - noinline handler: suspend E.(E) -> Unit, - ): Listener = subscribeOnce(E::class, coroutineContext, priority, handler) - - /** - * @see subscribeOnce - */ - public actual fun subscribeOnce( - eventClass: KClass, - coroutineContext: CoroutineContext, - priority: EventPriority, - handler: suspend E.(E) -> Unit, - ): Listener = subscribeInternal( - eventClass, - createListener0(coroutineContext, ConcurrencyKind.LOCKED, priority) { it.handler(it); ListeningStatus.STOPPED } - ) - - // endregion - - /** - * 注册 [ListenerHost] 中的所有 [EventHandler] 标注的方法到这个 [EventChannel]. 查看 [EventHandler]. - * - * @param coroutineContext 在 [defaultCoroutineContext] 的基础上, 给事件监听协程的额外的 [CoroutineContext] - * - * @see subscribe - * @see EventHandler - * @see ListenerHost - */ - @JvmOverloads - public fun registerListenerHost( - host: ListenerHost, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - ) { - val jobOfListenerHost: Job? - val coroutineContext0 = if (host is SimpleListenerHost) { - val listenerCoroutineContext = host.coroutineContext - val listenerJob = listenerCoroutineContext[Job] - - val rsp = listenerCoroutineContext.minusKey(Job) + - coroutineContext + - (listenerCoroutineContext[CoroutineExceptionHandler] ?: EmptyCoroutineContext) - - val registerCancelHook = when { - listenerJob === null -> false - - // Registering cancellation hook is needless - // if [Job] of [EventChannel] is same as [Job] of [SimpleListenerHost] - (rsp[Job] ?: this.defaultCoroutineContext[Job]) === listenerJob -> false - - else -> true - } - - jobOfListenerHost = if (registerCancelHook) { - listenerCoroutineContext[Job] - } else { - null - } - rsp - } else { - jobOfListenerHost = null - coroutineContext - } - for (method in host.javaClass.declaredMethods) { - method.getAnnotation(EventHandler::class.java)?.let { - val listener = method.registerEventHandler(host, this, it, coroutineContext0) - // For [SimpleListenerHost.cancelAll] - jobOfListenerHost?.invokeOnCompletion { exception -> - listener.cancel( - when (exception) { - is CancellationException -> exception - is Throwable -> CancellationException(null, exception) - else -> null - } - ) - } - } - } - } - - // region Java API - - /** - * Java API. 查看 [subscribeAlways] 获取更多信息. - * - * ```java - * eventChannel.subscribeAlways(GroupMessageEvent.class, (event) -> { }); - * ``` - * - * @see subscribe - * @see subscribeAlways - */ - @JvmOverloads - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public fun subscribeAlways( - eventClass: Class, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - concurrency: ConcurrencyKind = CONCURRENT, - priority: EventPriority = EventPriority.NORMAL, - handler: Consumer, - ): Listener = subscribeInternal( - eventClass.kotlin, - createListener0(coroutineContext, concurrency, priority) { event -> - runInterruptible(Dispatchers.IO) { handler.accept(event) } - ListeningStatus.LISTENING - } - ) - - /** - * Java API. 查看 [subscribe] 获取更多信息. - * - * ```java - * eventChannel.subscribe(GroupMessageEvent.class, (event) -> { - * return ListeningStatus.LISTENING; - * }); - * ``` - * - * @see subscribe - */ - @JvmOverloads - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public fun subscribe( - eventClass: Class, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - concurrency: ConcurrencyKind = CONCURRENT, - priority: EventPriority = EventPriority.NORMAL, - handler: java.util.function.Function, - ): Listener = subscribeInternal( - eventClass.kotlin, - createListener0(coroutineContext, concurrency, priority) { event -> - runInterruptible(Dispatchers.IO) { handler.apply(event) } - } - ) - - /** - * Java API. 查看 [subscribeOnce] 获取更多信息. - * - * ```java - * eventChannel.subscribeOnce(GroupMessageEvent.class, (event) -> { }); - * ``` - * - * @see subscribe - * @see subscribeOnce - */ - @JvmOverloads - @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") - @kotlin.internal.LowPriorityInOverloadResolution - public fun subscribeOnce( - eventClass: Class, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - concurrency: ConcurrencyKind = CONCURRENT, - priority: EventPriority = EventPriority.NORMAL, - handler: Consumer, - ): Listener = subscribeInternal( - eventClass.kotlin, - createListener0(coroutineContext, concurrency, priority) { event -> - runInterruptible(Dispatchers.IO) { handler.accept(event) } - ListeningStatus.STOPPED - } - ) - - // endregion - - // region deprecated - - /** - * 创建事件监听并将监听结果发送在 [Channel]. 将返回值 [Channel] [关闭][Channel.close] 时将会同时关闭事件监听. - * - * ## 已弃用 - * - * 请使用 [forwardToChannel] 替代. - * - * @param capacity Channel 容量. 详见 [Channel] 构造. - * - * @see subscribeAlways - * @see Channel - */ - @Deprecated( - "Please use forwardToChannel instead.", - replaceWith = ReplaceWith( - "Channel(capacity).apply { forwardToChannel(this, coroutineContext, priority) }", - "kotlinx.coroutines.channels.Channel" - ), - level = DeprecationLevel.ERROR, - ) - @DeprecatedSinceMirai(warningSince = "2.10", errorSince = "2.14") - @MiraiExperimentalApi - public fun asChannel( - capacity: Int = Channel.RENDEZVOUS, - coroutineContext: CoroutineContext = EmptyCoroutineContext, - @Suppress("UNUSED_PARAMETER") concurrency: ConcurrencyKind = CONCURRENT, - priority: EventPriority = EventPriority.NORMAL, - ): Channel = - Channel(capacity).apply { forwardToChannel(this, coroutineContext, priority) } - - // endregion - - // region impl - - - // protected, to hide from users - @MiraiInternalApi - protected actual abstract fun registerListener(eventClass: KClass, listener: Listener) - - // to overcome visibility issue - @OptIn(MiraiInternalApi::class) - internal actual fun registerListener0(eventClass: KClass, listener: Listener) { - return registerListener(eventClass, listener) - } - - @OptIn(MiraiInternalApi::class) - private fun , E : Event> subscribeInternal(eventClass: KClass, listener: L): L { - registerListener(eventClass, listener) - return listener - } - - /** - * Creates [Listener] instance using the [listenerBlock] action. - */ -// @Contract("_ -> new") // always creates new instance - @MiraiInternalApi - protected actual abstract fun createListener( - coroutineContext: CoroutineContext, - concurrencyKind: ConcurrencyKind, - priority: EventPriority, - listenerBlock: suspend (E) -> ListeningStatus, - ): Listener - - // to overcome visibility issue - @OptIn(MiraiInternalApi::class) - internal actual fun createListener0( - coroutineContext: CoroutineContext, - concurrencyKind: ConcurrencyKind, - priority: EventPriority, - listenerBlock: suspend (E) -> ListeningStatus, - ): Listener = createListener(coroutineContext, concurrencyKind, priority, listenerBlock) - - // endregion -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt b/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt deleted file mode 100644 index afc16ed839..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/event/MessageSelectBuilderUnit.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.event - -import net.mamoe.mirai.event.events.MessageEvent -import net.mamoe.mirai.message.data.Message -import net.mamoe.mirai.utils.MiraiInternalApi - -/** - * [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器. - * - * 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL - * - * @see MessageSubscribersBuilder 查看上层 API - */ -@OptIn(MiraiInternalApi::class) -public actual abstract class MessageSelectBuilderUnit @PublishedApi internal actual constructor( - ownerMessagePacket: M, - stub: Any?, - subscriber: (M.(String) -> Boolean, MessageListener) -> Unit -) : CommonMessageSelectBuilderUnit(ownerMessagePacket, stub, subscriber) { - @JvmName("timeout-ncvN2qU") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker { - return timeout(timeoutMillis) - } - - @Suppress("unused") - @JvmName("invoke-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) { - return invoke(block) - } - - @Suppress("unused") - @JvmName("invoke-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? { - invoke(block) - return null - } - - @JvmName("reply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) { - return reply(block) - } - - @JvmName("reply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? { - reply(block) - return null - } - - @JvmName("reply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(message: String) { - return reply(message) - } - - @JvmName("reply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? { - reply(message) - return null - } - - @JvmName("reply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) { - return reply(message) - } - - @JvmName("reply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? { - reply(message) - return null - } - - - @JvmName("quoteReply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) { - return reply(block) - } - - @JvmName("quoteReply-RNyhSv4") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? { - reply(block) - return null - } - - @JvmName("quoteReply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) { - return reply(message) - } - - @JvmName("quoteReply-sCZ5gAI") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? { - reply(message) - return null - } - - @JvmName("quoteReply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) { - return reply(message) - } - - @JvmName("quoteReply-AVDwu3U") - @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) - public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? { - reply(message) - return null - } -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/event/JvmMethodListenersInternal.kt b/mirai-core-api/src/jvmBaseMain/kotlin/internal/event/JvmMethodListenersInternal.kt deleted file mode 100644 index 62f92ba9d4..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/internal/event/JvmMethodListenersInternal.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2019-2022 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.internal.event - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import net.mamoe.mirai.event.* -import net.mamoe.mirai.utils.EventListenerLikeJava -import net.mamoe.mirai.utils.castOrNull -import java.lang.reflect.Method -import kotlin.coroutines.CoroutineContext -import kotlin.reflect.KClass -import kotlin.reflect.full.IllegalCallableAccessException -import kotlin.reflect.full.callSuspend -import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.jvm.isAccessible -import kotlin.reflect.jvm.kotlinFunction - - -private fun Method.isKotlinFunction(): Boolean { - - if (getDeclaredAnnotation(EventListenerLikeJava::class.java) != null) return false - if (declaringClass.getDeclaredAnnotation(EventListenerLikeJava::class.java) != null) return false - - @Suppress("RemoveRedundantQualifierName") // for strict - return declaringClass.getDeclaredAnnotation(kotlin.Metadata::class.java) != null -} - -@Suppress("UNCHECKED_CAST") -internal fun Method.registerEventHandler( - owner: Any, - eventChannel: EventChannel<*>, - annotation: EventHandler, - coroutineContext: CoroutineContext, -): Listener { - this.isAccessible = true - val kotlinFunction = kotlin.runCatching { this.kotlinFunction }.getOrNull() - return if (kotlinFunction != null && isKotlinFunction()) { - // kotlin functions - - val param = kotlinFunction.parameters - when (param.size) { - 3 -> { // ownerClass, receiver, event - check(param[1].type == param[2].type) { "Illegal kotlin function ${kotlinFunction.name}. Receiver and param must have same type" } - check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { - "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" - } - } - 2 -> { // ownerClass, event - check((param[1].type.classifier as? KClass<*>)?.isSubclassOf(Event::class) == true) { - "Illegal kotlin function ${kotlinFunction.name}. First param or receiver must be subclass of Event, but found ${param[1].type.classifier}" - } - } - else -> error("function ${kotlinFunction.name} must have one Event param") - } - lateinit var listener: Listener<*> - kotlin.runCatching { - kotlinFunction.isAccessible = true - } - suspend fun callFunction(event: Event): Any? { - try { - return when (param.size) { - 3 -> { - if (kotlinFunction.isSuspend) { - kotlinFunction.callSuspend(owner, event, event) - } else withContext(Dispatchers.IO) { // for safety - kotlinFunction.call(owner, event, event) - } - - } - 2 -> { - if (kotlinFunction.isSuspend) { - kotlinFunction.callSuspend(owner, event) - } else withContext(Dispatchers.IO) { // for safety - kotlinFunction.call(owner, event) - } - } - else -> error("stub") - } - } catch (e: IllegalCallableAccessException) { - listener.completeExceptionally(e) - return ListeningStatus.STOPPED - } catch (e: Throwable) { - throw ExceptionInEventHandlerException(event, cause = e) - } - } - require(!kotlinFunction.returnType.isMarkedNullable) { - "Kotlin event handlers cannot have nullable return type." - } - require(kotlinFunction.parameters.none { it.type.isMarkedNullable }) { - "Kotlin event handlers cannot have nullable parameter type." - } - when (kotlinFunction.returnType.classifier) { - Unit::class, Nothing::class -> { - eventChannel.subscribeAlways( - param[1].type.classifier as KClass, - coroutineContext, - annotation.concurrency, - annotation.priority - ) { - if (annotation.ignoreCancelled) { - if ((this as? CancellableEvent)?.isCancelled != true) { - callFunction(this) - } - } else callFunction(this) - }.also { listener = it } - } - ListeningStatus::class -> { - eventChannel.subscribe( - param[1].type.classifier as KClass, - coroutineContext, - annotation.concurrency, - annotation.priority - ) { - if (annotation.ignoreCancelled) { - if ((this as? CancellableEvent)?.isCancelled != true) { - callFunction(this) as ListeningStatus - } else ListeningStatus.LISTENING - } else callFunction(this) as ListeningStatus - }.also { listener = it } - } - else -> error("Illegal method return type. Required Void, Nothing or ListeningStatus, found ${kotlinFunction.returnType.classifier}") - } - } else { - // java methods - - val paramType = this.parameterTypes[0] - check(this.parameterTypes.size == 1 && Event::class.java.isAssignableFrom(paramType)) { - "Illegal method parameter. Required one exact Event subclass. found ${this.parameterTypes.contentToString()}" - } - suspend fun callMethod(event: Event): Any? { - fun Method.invokeWithErrorReport(self: Any?, vararg args: Any?): Any? = try { - invoke(self, *args) - } catch (exception: IllegalArgumentException) { - throw IllegalArgumentException( - "Internal Error: $exception, method=${this}, this=$self, arguments=$args, please report to https://github.com/mamoe/mirai", - exception - ) - } catch (e: Throwable) { - throw ExceptionInEventHandlerException(event, cause = e) - } - - - return if (annotation.ignoreCancelled) { - if (event.castOrNull()?.isCancelled != true) { - withContext(Dispatchers.IO) { - this@registerEventHandler.invokeWithErrorReport(owner, event) - } - } else ListeningStatus.LISTENING - } else withContext(Dispatchers.IO) { - this@registerEventHandler.invokeWithErrorReport(owner, event) - } - } - - when (this.returnType) { - Void::class.java, Void.TYPE, Nothing::class.java -> { - eventChannel.subscribeAlways( - paramType.kotlin as KClass, - coroutineContext, - annotation.concurrency, - annotation.priority - ) { - callMethod(this) - } - } - ListeningStatus::class.java -> { - eventChannel.subscribe( - paramType.kotlin as KClass, - coroutineContext, - annotation.concurrency, - annotation.priority - ) { - callMethod(this) as ListeningStatus? - ?: error("Java method EventHandler cannot return `null`: $this") - } - } - else -> error("Illegal method return type. Required Void or ListeningStatus, but found ${this.returnType.canonicalName}") - } - } -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/message/overwritePolymorphicWith.kt b/mirai-core-api/src/jvmBaseMain/kotlin/internal/message/overwritePolymorphicWith.kt deleted file mode 100644 index 12cff2a084..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/internal/message/overwritePolymorphicWith.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2019-2022 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.internal.message - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.overwriteWith -import kotlinx.serialization.modules.polymorphic -import net.mamoe.mirai.message.data.SingleMessage -import kotlin.reflect.KClass -import kotlin.reflect.full.allSuperclasses -import kotlin.reflect.full.isSubclassOf - -internal actual fun SerializersModule.overwritePolymorphicWith( - type: KClass, - serializer: KSerializer -): SerializersModule { - return overwriteWith(SerializersModule { - // contextual(type, serializer) - for (superclass in type.allSuperclasses) { - if (superclass.isFinal) continue - if (!superclass.isSubclassOf(SingleMessage::class)) continue - @Suppress("UNCHECKED_CAST") - polymorphic(superclass as KClass) { - subclass(type, serializer) - } - } - }) -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/Marker.kt b/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/Marker.kt deleted file mode 100644 index ea83b7c348..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/internal/utils/Marker.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2019-2022 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.internal.utils - -import org.apache.logging.log4j.MarkerManager - -@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility -internal actual typealias Marker = org.apache.logging.log4j.Marker - -internal actual object MarkerManager { - actual fun getMarker(name: String): Marker { - return MarkerManager.getMarker(name) - } -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/message/action/AsyncRecallResult.kt b/mirai-core-api/src/jvmBaseMain/kotlin/message/action/AsyncRecallResult.kt deleted file mode 100644 index 75491cb97c..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/message/action/AsyncRecallResult.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmBlockingBridge - -package net.mamoe.mirai.message.action - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.future.asCompletableFuture -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.message.data.MessageSource -import net.mamoe.mirai.message.data.MessageSource.Key.recallIn -import java.util.concurrent.CompletableFuture - - -/** - * 异步撤回结果. - * - * 可由 [MessageSource.recallIn] 返回得到. - * - * ## Kotlin 用法示例 - * - * ### 获取撤回失败时的异常 - * - * ``` - * val exception = result.exception.await() // 挂起协程并等待撤回的结果. - * if (exception == null) { - * // 撤回成功 - * } else { - * // 撤回失败 - * } - * ``` - * - * 若仅需要了解撤回是否成功而不需要获取详细异常实例, 可使用 [isSuccess] - * - * ## Java 用法示例 - * - * ```java - * Throwable exception = result.exceptionFuture.get(); // 阻塞线程并等待撤回的结果. - * if (exception == null) { - * // 撤回成功 - * } else { - * // 撤回失败 - * } - * ``` - * - * @see MessageSource.recallIn - */ -public actual class AsyncRecallResult internal actual constructor( - /** - * 撤回时产生的异常, 当撤回成功时为 `null`. Kotlin [Deferred] API. - */ - public actual val exception: Deferred, -) { - /** - * 撤回时产生的异常, 当撤回成功时为 `null`. Java [CompletableFuture] API. - */ - public val exceptionFuture: CompletableFuture by lazy { exception.asCompletableFuture() } - - /** - * 撤回是否成功. Kotlin [Deferred] API. - */ - public actual val isSuccess: Deferred by lazy { - CompletableDeferred().apply { - exception.invokeOnCompletion { - complete(it == null) - } - } - } - - /** - * 撤回是否成功. Java [CompletableFuture] API. - */ - public val isSuccessFuture: CompletableFuture by lazy { isSuccess.asCompletableFuture() } - - /** - * 挂起协程 (在 Java 为阻塞线程) 直到撤回完成, 返回撤回时产生的异常. 当撤回成功时返回 `null`. - */ - public actual suspend fun awaitException(): Throwable? { - return exception.await() - } - - /** - * 挂起协程 (在 Java 为阻塞线程) 直到撤回完成, 返回撤回的结果. - */ - public actual suspend fun awaitIsSuccess(): Boolean { - return isSuccess.await() - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt deleted file mode 100644 index 7456b5c933..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/message/data/FileMessage.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.message.data - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.FileSupported -import net.mamoe.mirai.contact.file.AbsoluteFile -import net.mamoe.mirai.event.events.MessageEvent -import net.mamoe.mirai.message.code.CodableMessage -import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode -import net.mamoe.mirai.message.data.visitor.MessageVisitor -import net.mamoe.mirai.utils.* - -/** - * 文件消息. - * - * [name] 与 [size] 只供本地使用, 发送消息时只会使用 [id] 和 [internalId]. - * - * 注: [FileMessage] 不可二次发送 - * - * ### 文件操作 - * 要下载这个文件, 可通过 [toAbsoluteFile] 获取到 [AbsoluteFile] 然后操作. - * - * 要获取到 [FileMessage], 可以通过 [MessageEvent.message] 获取, 或通过 [AbsoluteFile.toMessage] 得到. - * - * @since 2.5 - * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. - */ -@Serializable(FileMessage.Serializer::class) -@SerialName(FileMessage.SERIAL_NAME) -@NotStableForInheritance -@JvmBlockingBridge -public actual interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { - /** - * 服务器需要的某种 ID. - */ - public actual val id: String - - /** - * 服务器需要的某种 ID. - */ - public actual val internalId: Int - - /** - * 文件名 - */ - public actual val name: String - - /** - * 文件大小 bytes - */ - public actual val size: Long - - actual override fun contentToString(): String = "[文件]$name" // orthodox - - @MiraiExperimentalApi - actual override fun appendMiraiCodeTo(builder: StringBuilder) { - builder.append("[mirai:file:") - builder.appendStringAsMiraiCode(id).append(",") - builder.append(internalId).append(",") - builder.appendStringAsMiraiCode(name).append(",") - builder.append(size).append("]") - } - - /** - * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. - */ - @Suppress("DEPRECATION_ERROR") - @Deprecated( - "Please use toAbsoluteFile", - ReplaceWith("this.toAbsoluteFile(contact)"), - level = DeprecationLevel.ERROR - ) // deprecated since 2.8.0-RC - @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") - public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { - return contact.filesRoot.resolveById(id) - } - - /** - * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`. - * - * @since 2.8 - */ - public actual suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? - - actual override val key: Key get() = Key - - @MiraiInternalApi - actual override fun accept(visitor: MessageVisitor, data: D): R { - return visitor.visitFileMessage(this, data) - } - - /** - * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. - */ - public actual companion object Key : - AbstractPolymorphicMessageKey( - MessageContent, { it.safeCast() }) { - - public actual const val SERIAL_NAME: String = "FileMessage" - - /** - * 构造 [FileMessage] - * @since 2.5 - */ - @JvmStatic - public actual fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = - Mirai.createFileMessage(id, internalId, name, size) - } - - - public actual object Serializer : - KSerializer by @OptIn(MiraiInternalApi::class) FallbackFileMessageSerializer() -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/message/data/MessageChainJvm.kt b/mirai-core-api/src/jvmBaseMain/kotlin/message/data/MessageChainJvm.kt deleted file mode 100644 index 0334fb1463..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/message/data/MessageChainJvm.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmMultifileClass -@file:JvmName("MessageUtils") - -package net.mamoe.mirai.message.data - -import java.util.stream.Stream -import kotlin.streams.asSequence - - -/** - * 扁平化 [this] 并创建一个 [MessageChain]. - */ -@JvmName("newChain") -public fun Stream.toMessageChain(): MessageChain = this.asSequence().toMessageChain() - - diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/package.kt b/mirai-core-api/src/jvmBaseMain/kotlin/package.kt deleted file mode 100644 index 640d460926..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/package.kt +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2019-2022 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 \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt deleted file mode 100644 index 7c68702438..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/BotConfiguration.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.utils - -import net.mamoe.mirai.Bot -import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo -import java.io.File -import java.io.InputStream - -/** - * [BotConfiguration] 的 JVM 平台特别配置 - * @since 2.15 - */ -@NotStableForInheritance -public actual abstract class AbstractBotConfiguration { // open for Java - protected actual abstract var deviceInfo: ((Bot) -> DeviceInfo)? - protected actual abstract var networkLoggerSupplier: ((Bot) -> MiraiLogger) - protected actual abstract var botLoggerSupplier: ((Bot) -> MiraiLogger) - - - /** - * 工作目录. 默认为 "." - */ - public var workingDir: File = File(".") - - /////////////////////////////////////////////////////////////////////////// - // Device - /////////////////////////////////////////////////////////////////////////// - - /** - * 使用文件存储设备信息. - * - * 此函数只在 JVM 和 Android 有效. 在其他平台将会抛出异常. - * @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json". - * @see deviceInfo - */ - @JvmOverloads - @BotConfiguration.ConfigurationDsl - public actual fun fileBasedDeviceInfo(filepath: String) { - deviceInfo = { - workingDir.resolve(filepath).loadAsDeviceInfo(BotConfiguration.json) - } - } - - /////////////////////////////////////////////////////////////////////////// - // Logging - /////////////////////////////////////////////////////////////////////////// - - - /** - * 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs]) - * 默认目录路径为 "$workingDir/logs/". - * @see DirectoryLogger - * @see redirectNetworkLogToDirectory - */ - @JvmOverloads - @BotConfiguration.ConfigurationDsl - public fun redirectNetworkLogToDirectory( - dir: File = File("logs"), - retain: Long = 1.weeksToMillis, - identity: (bot: Bot) -> String = { "Net ${it.id}" } - ) { - require(!dir.isFile) { "dir must not be a file" } - networkLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) } - } - - /** - * 重定向 [网络日志][networkLoggerSupplier] 到指定文件. 默认文件路径为 "$workingDir/mirai.log". - * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile]) - * @see SingleFileLogger - * @see redirectNetworkLogToDirectory - */ - @JvmOverloads - @BotConfiguration.ConfigurationDsl - public fun redirectNetworkLogToFile( - file: File = File("mirai.log"), - identity: (bot: Bot) -> String = { "Net ${it.id}" } - ) { - require(!file.isDirectory) { "file must not be a dir" } - networkLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) } - } - - /** - * 重定向 [Bot 日志][botLoggerSupplier] 到指定文件. - * 日志将会逐行追加到此文件. 若文件不存在将会自动创建 ([File.createNewFile]) - * @see SingleFileLogger - * @see redirectBotLogToDirectory - */ - @JvmOverloads - @BotConfiguration.ConfigurationDsl - public fun redirectBotLogToFile( - file: File = File("mirai.log"), - identity: (bot: Bot) -> String = { "Bot ${it.id}" } - ) { - require(!file.isDirectory) { "file must not be a dir" } - botLoggerSupplier = { SingleFileLogger(identity(it), workingDir.resolve(file)) } - } - - - /** - * 重定向 [Bot 日志][botLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs]) - * @see DirectoryLogger - * @see redirectBotLogToFile - */ - @JvmOverloads - @BotConfiguration.ConfigurationDsl - public fun redirectBotLogToDirectory( - dir: File = File("logs"), - retain: Long = 1.weeksToMillis, - identity: (bot: Bot) -> String = { "Bot ${it.id}" } - ) { - require(!dir.isFile) { "dir must not be a file" } - botLoggerSupplier = { DirectoryLogger(identity(it), workingDir.resolve(dir), retain) } - } - - /////////////////////////////////////////////////////////////////////////// - // Cache - ////////////////////////////////////////////////////////////////////////// - - /** - * 缓存数据目录, 相对于 [workingDir]. - * - * 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行. - * 默认启用的缓存可以加快登录过程. - * - * 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含: - * - 联系人列表 - * - 登录服务器列表 - * - 资源服务秘钥 - * - * 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件. - * - * @since 2.4 - */ - public var cacheDir: File = File("cache") - - /////////////////////////////////////////////////////////////////////////// - // Misc - /////////////////////////////////////////////////////////////////////////// - - internal actual fun applyMppCopy(new: BotConfiguration) { - new.workingDir = workingDir - new.cacheDir = cacheDir - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/ExternalResource.kt deleted file mode 100644 index 0021745e4d..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/ExternalResource.kt +++ /dev/null @@ -1,626 +0,0 @@ -/* - * Copyright 2019-2022 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.utils - -import io.ktor.utils.io.core.Input -import io.ktor.utils.io.errors.* -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.Mirai -import net.mamoe.mirai.contact.Contact -import net.mamoe.mirai.contact.Contact.Companion.sendImage -import net.mamoe.mirai.contact.Contact.Companion.uploadImage -import net.mamoe.mirai.contact.FileSupported -import net.mamoe.mirai.contact.Group -import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray -import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile -import net.mamoe.mirai.message.MessageReceipt -import net.mamoe.mirai.message.data.FileMessage -import net.mamoe.mirai.message.data.Image -import net.mamoe.mirai.message.data.sendTo -import net.mamoe.mirai.message.data.toVoice -import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo -import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage -import java.io.Closeable -import java.io.File -import java.io.InputStream -import java.io.RandomAccessFile -import kotlin.io.use - -/** - * 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现. - * - * [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据. - * - * # 创建 - * - [File.toExternalResource] - * - [RandomAccessFile.toExternalResource] - * - [ByteArray.toExternalResource] - * - [InputStream.toExternalResource] - * - * ## 在 Kotlin 获得和使用 [ExternalResource] 实例 - * - * ``` - * file.toExternalResource().use { resource -> // 安全地使用资源 - * contact.uploadImage(resource) // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", resource) // 或者用来上传文件 - * } - * ``` - * - * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例: - * - * ``` - * inputStream.use { input -> // 安全地使用 InputStream - * input.toExternalResource().use { resource -> // 安全地使用资源 - * contact.uploadImage(resource) // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", resource) // 或者用来上传文件 - * } - * } - * ``` - * - * ## 在 Java 获得和使用 [ExternalResource] 实例 - * - * ``` - * try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file - * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", resource); // 或者用来上传文件 - * } - * ``` - * - * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例: - * - * ```java - * try (InputStream stream = ...) { // 安全地使用 InputStream - * try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源 - * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", resource); // 或者用来上传文件 - * } - * } - * ``` - * - * # 释放 - * - * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]). - * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close]. - * - * ## 未释放资源的补救策略 - * - * 自 2.7 起, 每个 mirai 内置的 [ExternalResource] 实现都有引用跟踪, 当 [ExternalResource] 被 GC 后会执行被动释放. - * 这依赖于 JVM 垃圾收集策略, 因此不可靠, 资源仍然需要手动 close. - * - * ## 使用单次自动释放 - * - * 若创建的资源仅需要*很快地*使用一次, 可使用 [toAutoCloseable] 获得在使用一次后就会自动关闭的资源. - * - * 示例: - * ```java - * contact.uploadImage(ExternalResource.create(file).toAutoCloseable()); // 创建并立即使用单次自动释放的资源 - * ``` - * - * **注意**: 如果仅使用 [toAutoCloseable] 而不通过 [Contact.uploadImage] 等 mirai 内置方法使用资源, 资源仍然会处于打开状态且不会被自动关闭. - * 最终资源会由上述*未释放资源的补救策略*关闭, 但这依赖于 JVM 垃圾收集策略而不可靠. - * 因此建议在创建单次自动释放的资源后就尽快使用它, 否则仍然需要考虑在正确的时间及时关闭资源. - * - * # 实现 [ExternalResource] - * - * 可以自行实现 [ExternalResource]. 但通常上述创建方法已足够使用. - * - * 建议继承 [AbstractExternalResource], 这将支持上文提到的资源自动释放功能. - * - * 实现时需保持 [ExternalResource] 在构造后就不可变, 并且所有属性都总是返回一个固定值. - * - * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image] - * @see ExternalResource.sendAsImageTo 将资源作为图片发送 - * @see Contact.uploadImage 上传一个资源作为图片, 得到 [Image] - * @see Contact.sendImage 发送一个资源作为图片 - * - * @see FileCacheStrategy - */ -public actual interface ExternalResource : Closeable { - - /** - * 是否在 _使用一次_ 后自动 [close]. - * - * 该属性仅供调用方参考. 如 [Contact.uploadImage] 会在方法结束时关闭 [isAutoClose] 为 `true` 的 [ExternalResource], 无论上传图片是否成功. - * - * 所有 mirai 内置的上传图片, 上传语音等方法都支持该行为. - * - * @since 2.8 - */ - public actual val isAutoClose: Boolean - get() = false - - /** - * 文件内容 MD5. 16 bytes - */ - public actual val md5: ByteArray - - /** - * 文件内容 SHA1. 16 bytes - * @since 2.5 - */ - public actual val sha1: ByteArray - get() = - throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}") - // 如果你要实现 [ExternalResource], 你也应该实现 [sha1]. - // 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现. - - - /** - * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME]. - * - * 默认会从文件头识别, 支持的文件类型: - * png, jpg, gif, tif, bmp, amr, silk - * - * @see net.mamoe.mirai.utils.getFileType - * @see net.mamoe.mirai.utils.FILE_TYPES - * @see DEFAULT_FORMAT_NAME - */ - public actual val formatName: String - - /** - * 文件大小 bytes - */ - public actual val size: Long - - /** - * 当 [close] 时会 [CompletableDeferred.complete] 的 [Deferred]. - */ - public actual val closed: Deferred - - /** - * 打开 [InputStream]. 在返回的 [InputStream] 被 [关闭][InputStream.close] 前无法再次打开流. - * - * 关闭此流不会关闭 [ExternalResource]. - * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出 - */ - public fun inputStream(): InputStream - - /** - * 打开 [Input]. 在返回的 [Input] 被 [关闭][Input.close] 前无法再次打开流. - * 注意: 此 API 不稳定, 请使用 [inputStream] 代替. - * - * 关闭此流不会关闭 [ExternalResource]. - * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出 - * - * @since 2.13 - */ - @MiraiInternalApi - public actual fun input(): Input - - @MiraiInternalApi - public actual fun calculateResourceId(): String { - return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME }) - } - - /** - * 该 [ExternalResource] 的数据来源, 可能有以下的返回 - * - * - [File] 本地文件 - * - [java.nio.file.Path] 某个具体文件路径 - * - [java.nio.ByteBuffer] RAM - * - [java.net.URI] uri - * - [ByteArray] RAM - * - Or more... - * - * implementation note: - * - * - 对于无法二次读取的数据来源 (如 [InputStream]), 返回 `null` - * - 对于一个来自网络的资源, 请返回 [java.net.URI] (not URL, 或者其他库的 URI/URL 类型) - * - 不要返回 [String], 没有约定 [String] 代表什么 - * - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如 [RandomAccessFile]) - * - * @since 2.8.0 - */ - public actual val origin: Any? get() = null - - /** - * 创建一个在 _使用一次_ 后就会自动 [close] 的 [ExternalResource]. - * - * @since 2.8.0 - */ - public actual fun toAutoCloseable(): ExternalResource { - return if (isAutoClose) this else { - val delegate = this - object : ExternalResource by delegate { - override val isAutoClose: Boolean get() = true - override fun toString(): String = "ExternalResourceWithAutoClose(delegate=$delegate)" - override fun toAutoCloseable(): ExternalResource { - return this - } - } - } - } - - - public actual companion object { - /** - * 在无法识别文件格式时使用的默认格式名. "mirai". - * - * @see ExternalResource.formatName - */ - public actual const val DEFAULT_FORMAT_NAME: String = "mirai" - - /////////////////////////////////////////////////////////////////////////// - // region toExternalResource - /////////////////////////////////////////////////////////////////////////// - - /** - * **打开文件**并创建 [ExternalResource]. - * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. - * - * 将以只读模式打开这个文件 (因此文件会处于被占用状态), 直到 [ExternalResource.close]. - * - * @param formatName 查看 [ExternalResource.formatName] - */ - @JvmStatic - @JvmOverloads - @JvmName("create") - public fun File.toExternalResource(formatName: String? = null): ExternalResource = - // although RandomAccessFile constructor throws IOException, actual performance influence is minor so not propagating IOException - RandomAccessFile(this, "r").toExternalResource(formatName).also { - it.cast().origin = this@toExternalResource - } - - /** - * 创建 [ExternalResource]. - * 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭, 届时将会关闭 [RandomAccessFile]. - * - * **注意**:若关闭 [RandomAccessFile], 也会间接关闭 [ExternalResource]. - * - * @see closeOriginalFileOnClose 若为 `true`, 在 [ExternalResource.close] 时将会同步关闭 [RandomAccessFile]. 否则不会. - * - * @param formatName 查看 [ExternalResource.formatName] - */ - @JvmStatic - @JvmOverloads - @JvmName("create") - public fun RandomAccessFile.toExternalResource( - formatName: String? = null, - closeOriginalFileOnClose: Boolean = true, - ): ExternalResource = - ExternalResourceImplByFile(this, formatName, closeOriginalFileOnClose) - - /** - * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. - * - * @param formatName 查看 [ExternalResource.formatName] - */ - @JvmStatic - @JvmOverloads - @JvmName("create") - public actual fun ByteArray.toExternalResource(formatName: String?): ExternalResource = - ExternalResourceImplByByteArray(this, formatName) - - - /** - * 立即使用 [FileCacheStrategy] 缓存 [InputStream] 并创建 [ExternalResource]. - * 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭. - * - * **注意**:本函数不会关闭流. - * - * ### 在 Java 获得和使用 [ExternalResource] 实例 - * - * ``` - * try(ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file - * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 - * } - * ``` - * - * 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例: - * - * ``` - * try(InputStream stream = ...) { - * try(ExternalResource resource = ExternalResource.create(stream)) { - * contact.uploadImage(resource); // 用来上传图片 - * contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件 - * } - * } - * ``` - * - * - * @param formatName 查看 [ExternalResource.formatName] - * @see ExternalResource - */ - @JvmStatic - @JvmOverloads - @JvmName("create") - @Throws(IOException::class) // not in BIO context so propagate IOException - public fun InputStream.toExternalResource(formatName: String? = null): ExternalResource = - Mirai.FileCacheStrategy.newCache(this, formatName) - - // endregion - - - /* note: - 于 2.8.0-M1 添加 (#1392) - - 于 2.8.0-RC 移动至 `toExternalResource`(#1588) - */ - @JvmName("createAutoCloseable") - @JvmStatic - @Deprecated( - level = DeprecationLevel.HIDDEN, - message = "Moved to `toExternalResource()`", - replaceWith = ReplaceWith("resource.toAutoCloseable()"), - ) - @DeprecatedSinceMirai(errorSince = "2.8", hiddenSince = "2.10") - public fun createAutoCloseable(resource: ExternalResource): ExternalResource { - return resource.toAutoCloseable() - } - - /////////////////////////////////////////////////////////////////////////// - // region sendAsImageTo - /////////////////////////////////////////////////////////////////////////// - - /** - * 将图片作为单独的消息发送给指定联系人. - * - * **注意**:本函数不会关闭 [ExternalResource]. - * - * @see Contact.uploadImage 上传图片 - * @see Contact.sendMessage 最终调用, 发送消息. - * - * @throws OverFileSizeMaxException - */ - @JvmBlockingBridge - @JvmStatic - @JvmName("sendAsImage") - public actual suspend fun ExternalResource.sendAsImageTo(contact: C): MessageReceipt = - contact.uploadImage(this).sendTo(contact) - - /** - * 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人. - * - * 注意:本函数不会关闭流. - * - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmBlockingBridge - @JvmName("sendAsImage") - @JvmOverloads - public suspend fun InputStream.sendAsImageTo( - contact: C, - formatName: String? = null, - ): MessageReceipt = - runBIO { - // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo - toExternalResource(formatName) - }.withUse { sendAsImageTo(contact) } - - /** - * 将文件作为图片发送到指定联系人. - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmBlockingBridge - @JvmName("sendAsImage") - @JvmOverloads - public suspend fun File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt { - require(this.exists() && this.canRead()) - return toExternalResource(formatName).withUse { sendAsImageTo(contact) } - } - - // endregion - - /////////////////////////////////////////////////////////////////////////// - // region uploadAsImage - /////////////////////////////////////////////////////////////////////////// - - /** - * 上传图片并构造 [Image]. 这个函数可能需消耗一段时间. - * - * **注意**:本函数不会关闭 [ExternalResource]. - * - * @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人. - * - * @see Contact.uploadImage 最终调用, 上传图片. - */ - @JvmStatic - @JvmBlockingBridge - public actual suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this) - - /** - * 读取 [InputStream] 到临时文件并将其作为图片上传后构造 [Image]. - * - * 注意:本函数不会关闭流. - * - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmBlockingBridge - @JvmOverloads - public suspend fun InputStream.uploadAsImage(contact: Contact, formatName: String? = null): Image = - // toExternalResource throws IOException however we're in BIO context so not propagating IOException to sendAsImageTo - runBIO { toExternalResource(formatName) }.withUse { uploadAsImage(contact) } - - // endregion - - /////////////////////////////////////////////////////////////////////////// - // region uploadAsFile - /////////////////////////////////////////////////////////////////////////// - - /** - * 将文件作为图片上传后构造 [Image]. - * - * @param formatName 查看 [ExternalResource.formatName] - * @throws OverFileSizeMaxException - */ - @JvmStatic - @JvmBlockingBridge - @JvmOverloads - public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image = - toExternalResource(formatName).withUse { uploadAsImage(contact) } - - /** - * 上传文件并获取文件消息. - * - * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. - * - * 需要调用方手动[关闭资源][ExternalResource.close]. - * - * ## 已弃用 - * 查看 [RemoteFile.upload] 获取更多信息. - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @since 2.5 - * @see RemoteFile.path - * @see RemoteFile.upload - */ - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - @JvmStatic - @JvmBlockingBridge - @JvmOverloads - @Deprecated( - "Use sendTo instead.", - ReplaceWith( - "this.sendTo(contact, path, callback)", - "net.mamoe.mirai.utils.ExternalResource.Companion.sendTo" - ), - level = DeprecationLevel.HIDDEN - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public suspend fun File.uploadTo( - contact: FileSupported, - path: String, - callback: RemoteFile.ProgressionCallback? = null, - ): FileMessage = toExternalResource().use { - contact.filesRoot.resolve(path).upload(it, callback) - } - - /** - * 上传文件并获取文件消息. - * - * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. - * - * 需要调用方手动[关闭资源][ExternalResource.close]. - * - * ## 已弃用 - * 查看 [RemoteFile.upload] 获取更多信息. - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @since 2.5 - * @see RemoteFile.path - * @see RemoteFile.upload - */ - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - @JvmStatic - @JvmBlockingBridge - @JvmName("uploadAsFile") - @JvmOverloads - @Deprecated( - "Use sendAsFileTo instead.", - ReplaceWith( - "this.sendAsFileTo(contact, path, callback)", - "net.mamoe.mirai.utils.ExternalResource.Companion.sendAsFileTo" - ), - level = DeprecationLevel.HIDDEN - ) // deprecated since 2.7-M1 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public suspend fun ExternalResource.uploadAsFile( - contact: FileSupported, - path: String, - callback: RemoteFile.ProgressionCallback? = null, - ): FileMessage { - return contact.filesRoot.resolve(path).upload(this, callback) - } - - // endregion - - /////////////////////////////////////////////////////////////////////////// - // region sendAsFileTo - /////////////////////////////////////////////////////////////////////////// - - /** - * 上传文件并发送文件消息. - * - * 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @since 2.5 - * @see RemoteFile.path - * @see RemoteFile.uploadAndSend - */ - @Suppress("DEPRECATION_ERROR", "DEPRECATION") - @Deprecated( - "Deprecated. Please use AbsoluteFolder.uploadNewFile", - ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), - level = DeprecationLevel.ERROR, - ) // deprecated since 2.8.0-RC - @JvmStatic - @JvmBlockingBridge - @JvmOverloads - @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") - public suspend fun File.sendTo( - contact: C, - path: String, - callback: RemoteFile.ProgressionCallback? = null, - ): MessageReceipt = toExternalResource().use { - contact.filesRoot.resolve(path).upload(it, callback).sendTo(contact) - } - - /** - * 上传文件并发送件消息. 如果要上传的文件格式是图片或者语音, 也会将它们作为文件上传而不会调整消息类型. - * - * 需要调用方手动[关闭资源][ExternalResource.close]. - * - * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' - * @since 2.5 - * @see RemoteFile.path - * @see RemoteFile.uploadAndSend - */ - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - @Deprecated( - "Deprecated. Please use AbsoluteFolder.uploadNewFile", - ReplaceWith("contact.files.uploadNewFile(path, this, callback)"), - level = DeprecationLevel.ERROR - ) // deprecated since 2.8.0-RC - @JvmStatic - @JvmBlockingBridge - @JvmName("sendAsFile") - @JvmOverloads - @DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.14") - public suspend fun ExternalResource.sendAsFileTo( - contact: C, - path: String, - callback: RemoteFile.ProgressionCallback? = null, - ): MessageReceipt { - return contact.filesRoot.resolve(path).upload(this, callback).sendTo(contact) - } - - // endregion - - /////////////////////////////////////////////////////////////////////////// - // region uploadAsVoice - /////////////////////////////////////////////////////////////////////////// - - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - @JvmBlockingBridge - @JvmStatic - @Deprecated( - "Use `contact.uploadAudio(resource)` instead", - level = DeprecationLevel.HIDDEN - ) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice { - @Suppress("DEPRECATION", "DEPRECATION_ERROR") - if (contact is Group) return contact.uploadAudio(this).toVoice() - else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice") - } - // endregion - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/FileCacheStrategy.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/FileCacheStrategy.kt deleted file mode 100644 index fdbd3bb217..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/FileCacheStrategy.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2019-2022 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.utils - -import io.ktor.utils.io.errors.* -import kotlinx.coroutines.Dispatchers -import net.mamoe.mirai.Bot -import net.mamoe.mirai.IMirai -import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo -import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource -import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage -import net.mamoe.mirai.utils.FileCacheStrategy.MemoryCache -import net.mamoe.mirai.utils.FileCacheStrategy.TempCache -import java.io.File -import java.io.InputStream - -/** - * 资源缓存策略. - * - * 由于上传资源时服务器要求提前给出 MD5 和文件大小等数据, 一些资源如 [InputStream] 需要首先缓存才能使用. - * - * 资源的缓存都是将 [InputStream] 缓存未 [ExternalResource]. 根据 [FileCacheStrategy] 实现不同, 可以以临时文件存储, 也可以在数据库或是内存按需存储. - * Mirai 内置的实现有 [内存存储][MemoryCache] 和 [临时文件存储][TempCache]. - * 操作 [ExternalResource.toExternalResource] 时将会使用 [IMirai.FileCacheStrategy]. 可以覆盖, 示例: - * ``` - * // Kotlin - * Mirai.FileCacheStrategy = FileCacheStrategy.TempCache() // 使用系统默认缓存路径, 也是默认的行为 - * Mirai.FileCacheStrategy = FileCacheStrategy.TempCache(File("C:/cache")) // 使用自定义缓存路径 - * - * // Java - * Mirai.getInstance().setFileCacheStrategy(new FileCacheStrategy.TempCache()); // 使用系统默认缓存路径, 也是默认的行为 - * Mirai.getInstance().setFileCacheStrategy(new FileCacheStrategy.TempCache(new File("C:/cache"))); // 使用自定义的缓存路径 - * ``` - * - * 此接口的实现和使用都是稳定的. 自行实现的 [FileCacheStrategy] 也可以被 Mirai 使用. - * - * 注意, 此接口目前仅缓存 [InputStream] 等一次性数据. 好友列表等数据由每个 [Bot] 的 [BotConfiguration.cacheDir] 缓存. - * - * ### 使用 [FileCacheStrategy] 的操作 - * - [ExternalResource.toExternalResource] - * - [ExternalResource.uploadAsImage] - * - [ExternalResource.sendAsImageTo] - * - * @see ExternalResource - */ -public actual interface FileCacheStrategy { - /** - * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. - * - * 注意: - * - 此函数不会关闭输入 - * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). - * - * @param formatName 文件类型. 此参数通常只会影响官方客户端接收到的文件的文件后缀. 若为 `null` 则会自动根据文件头识别. 识别失败时将使用 "mirai" - */ - @Throws(IOException::class) - public fun newCache(input: InputStream, formatName: String? = null): ExternalResource - - /** - * 立即读取 [input] 所有内容并缓存为 [ExternalResource]. 自动根据文件头识别文件类型. 识别失败时将使用 "mirai". - * - * 注意: - * - 此函数不会关闭输入 - * - 此函数可能会阻塞线程读取 [input] 内容, 若在 Kotlin 协程使用请确保在允许阻塞的环境 ([Dispatchers.IO]). - */ - @Throws(IOException::class) - public fun newCache(input: InputStream): ExternalResource = newCache(input, null) - - /** - * 使用内存直接存储所有图片文件. 由 JVM 执行 GC. - */ - public object MemoryCache : FileCacheStrategy { - @Throws(IOException::class) - override fun newCache(input: InputStream, formatName: String?): ExternalResource { - return input.readBytes().toExternalResource(formatName) - } - } - - /** - * 使用系统临时文件夹缓存图片文件. 在图片使用完毕后或 JVM 正常结束时删除临时文件. - */ - public class TempCache @JvmOverloads public constructor( - /** - * 缓存图片存放位置. 为 `null` 时使用主机系统的临时文件夹: `File.createTempFile("tmp", null, directory)` - */ - public val directory: File? = null, - ) : FileCacheStrategy { - private fun createTempFile(): File { - return File.createTempFile("tmp", null, directory) - } - - @Throws(IOException::class) - override fun newCache(input: InputStream, formatName: String?): ExternalResource { - val file = createTempFile() - return file.apply { - deleteOnExit() - outputStream().use { out -> input.copyTo(out) } - }.toExternalResource(formatName).apply { - closed.invokeOnCompletion { - kotlin.runCatching { file.delete() } - } - } - } - } - - public actual companion object { - /** - * 当前平台下默认的缓存策略. 注意, 这可能不是 Mirai 全局默认使用的, Mirai 从 [IMirai.FileCacheStrategy] 获取. - * - * @see IMirai.FileCacheStrategy - */ - @MiraiExperimentalApi - @JvmStatic - public actual val PlatformDefault: FileCacheStrategy = TempCache(null) - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt deleted file mode 100644 index 21b3be6606..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/MiraiLogger.kt +++ /dev/null @@ -1,339 +0,0 @@ -/* - * 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 - */ - -@file:JvmMultifileClass -@file:JvmName("Utils") - -package net.mamoe.mirai.utils - -import kotlinx.atomicfu.atomic -import kotlinx.atomicfu.loop -import me.him188.kotlin.dynamic.delegation.dynamicDelegation -import net.mamoe.mirai.utils.* -import java.util.* -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.reflect.KClass - -/** - * 日志记录器. - * - * ## Mirai 日志系统 - * - * Mirai 内建简单的日志系统, 即 [MiraiLogger]. [MiraiLogger] 的实现有 [SimpleLogger], [PlatformLogger], [SilentLogger]. - * - * [MiraiLogger] 仅能处理简单的日志任务, 通常推荐使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 等日志库. - * - * ## 使用第三方日志库接管 Mirai 日志系统 - * - * 使用 [LoggerAdapters], 将第三方日志 `Logger` 转为 [MiraiLogger]. 然后通过 [MiraiLogger.Factory] 提供实现. - * - * ## 实现或使用 [MiraiLogger] - * - * 不建议实现或使用 [MiraiLogger]. 请优先考虑使用上述第三方框架. [MiraiLogger] 仅应用于兼容旧版本代码. - * - * @see SimpleLogger 简易 logger, 它将所有的日志记录操作都转移给 lambda `(String?, Throwable?) -> Unit` - * @see PlatformLogger 各个平台下的默认日志记录实现. - * @see SilentLogger 忽略任何日志记录操作的 logger 实例. - * @see LoggerAdapters - * - * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数. - */ -public actual interface MiraiLogger { - - /** - * 可以 service 实现的方式覆盖. - * - * @since 2.7 - */ - public actual interface Factory { - /** - * 创建 [MiraiLogger] 实例. - * - * @param requester 请求创建 [MiraiLogger] 的对象的 class - * @param identity 对象标记 (备注) - */ - public actual fun create(requester: KClass<*>, identity: String?): MiraiLogger = - this.create(requester.java, identity) - - /** - * 创建 [MiraiLogger] 实例. - * - * @param requester 请求创建 [MiraiLogger] 的对象的 class - * @param identity 对象标记 (备注) - */ - public fun create(requester: Class<*>, identity: String? = null): MiraiLogger - - /** - * 创建 [MiraiLogger] 实例. - * - * @param requester 请求创建 [MiraiLogger] 的对象 - */ - public actual fun create(requester: KClass<*>): MiraiLogger = create(requester, null) - - /** - * 创建 [MiraiLogger] 实例. - * - * @param requester 请求创建 [MiraiLogger] 的对象 - */ - public fun create(requester: Class<*>): MiraiLogger = create(requester, null) - - public actual companion object INSTANCE : - Factory by dynamicDelegation({ MiraiLoggerFactoryImplementationBridge }) - } - - public actual companion object { - /** - * 顶层日志, 仅供 Mirai 内部使用. - */ - @MiraiInternalApi - @MiraiExperimentalApi - @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") } - - /** - * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供. - */ - @Deprecated( - "Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory", - level = DeprecationLevel.HIDDEN - ) // deprecated since 2.7 - @JvmStatic - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.13") - public fun setDefaultLoggerCreator(@Suppress("UNUSED_PARAMETER") creator: (identity: String?) -> MiraiLogger) { - // nop - - -// DefaultFactoryOverrides.override { _, identity -> creator(identity) } - } - - /** - * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create]. - */ - @Deprecated( - "Please use MiraiLogger.Factory.create", ReplaceWith( - "MiraiLogger.Factory.create(YourClass::class, identity)", - "net.mamoe.mirai.utils.MiraiLogger" - ), level = DeprecationLevel.HIDDEN - ) // deprecated since 2.7 - @JvmStatic - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity) - } - - /** - * 日志的标记. 在 Mirai 中, identity 可为 - * - "Bot" - * - "BotNetworkHandler" - * 等. - * - * 它只用于帮助调试或统计. 十分建议清晰定义 identity - */ - public actual val identity: String? - - /** - * 获取 [MiraiLogger] 是否已开启 - * - * 除 [MiraiLoggerWithSwitch] 可控制开关外, 其他的所有 [MiraiLogger] 均一直开启. - */ - public actual val isEnabled: Boolean - - /** - * 当 VERBOSE 级别的日志启用时返回 `true`. - * - * 若 [isEnabled] 为 `false`, 返回 `false`. - * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. - * 其他情况下返回 [isEnabled] 的值. - * - * @since 2.7 - */ - public actual val isVerboseEnabled: Boolean get() = isEnabled - - /** - * 当 DEBUG 级别的日志启用时返回 `true` - * - * 若 [isEnabled] 为 `false`, 返回 `false`. - * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. - * 其他情况下返回 [isEnabled] 的值. - * - * @since 2.7 - */ - public actual val isDebugEnabled: Boolean get() = isEnabled - - /** - * 当 INFO 级别的日志启用时返回 `true` - * - * 若 [isEnabled] 为 `false`, 返回 `false`. - * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. - * 其他情况下返回 [isEnabled] 的值. - * - * @since 2.7 - */ - public actual val isInfoEnabled: Boolean get() = isEnabled - - /** - * 当 WARNING 级别的日志启用时返回 `true` - * - * 若 [isEnabled] 为 `false`, 返回 `false`. - * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. - * 其他情况下返回 [isEnabled] 的值. - * - * @since 2.7 - */ - public actual val isWarningEnabled: Boolean get() = isEnabled - - /** - * 当 ERROR 级别的日志启用时返回 `true` - * - * 若 [isEnabled] 为 `false`, 返回 `false`. - * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值. - * 其他情况下返回 [isEnabled] 的值. - * - * @since 2.7 - */ - public actual val isErrorEnabled: Boolean get() = isEnabled - - @Suppress("UNUSED_PARAMETER") - @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public var follower: MiraiLogger? - get() = null - set(value) {} - - /** - * 记录一个 `verbose` 级别的日志. - * 无关紧要的, 经常大量输出的日志应使用它. - */ - public actual fun verbose(message: String?) - - public actual fun verbose(e: Throwable?): Unit = verbose(null, e) - public actual fun verbose(message: String?, e: Throwable?) - - /** - * 记录一个 _调试_ 级别的日志. - */ - public actual fun debug(message: String?) - - public actual fun debug(e: Throwable?): Unit = debug(null, e) - public actual fun debug(message: String?, e: Throwable?) - - - /** - * 记录一个 _信息_ 级别的日志. - */ - public actual fun info(message: String?) - - public actual fun info(e: Throwable?): Unit = info(null, e) - public actual fun info(message: String?, e: Throwable?) - - - /** - * 记录一个 _警告_ 级别的日志. - */ - public actual fun warning(message: String?) - - public actual fun warning(e: Throwable?): Unit = warning(null, e) - public actual fun warning(message: String?, e: Throwable?) - - - /** - * 记录一个 _错误_ 级别的日志. - */ - public actual fun error(message: String?) - - public actual fun error(e: Throwable?): Unit = error(null, e) - public actual fun error(message: String?, e: Throwable?) - - /** 根据优先级调用对应函数 */ - - public actual fun call(priority: SimpleLogger.LogPriority, message: String?, e: Throwable?): Unit = - @OptIn(MiraiExperimentalApi::class) priority.correspondingFunction(this, message, e) - - @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 - @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") - public operator fun plus(follower: T): T = follower -} - -// used by Mirai Console -/** - * @since 2.13 - */ -internal object MiraiLoggerFactoryImplementationBridge : MiraiLogger.Factory { - private var _instance by lateinitMutableProperty { - createPlatformInstance() - } - - internal val instance get() = _instance - - // It is required for MiraiConsole because default implementation - // queries stdout on every message printing - // It creates an infinite loop (StackOverflowError) - internal var defaultLoggerFactory: (() -> MiraiLogger.Factory) = ::DefaultFactory - - fun createPlatformInstance() = loadService(MiraiLogger.Factory::class, defaultLoggerFactory) - - private val frozen = atomic(false) - - fun freeze(): Boolean { - return frozen.compareAndSet(expect = false, update = true) - } - - @TestOnly - fun reinit() { - defaultLoggerFactory = ::DefaultFactory - frozen.loop { value -> - _instance = createPlatformInstance() - if (frozen.compareAndSet(value, false)) return - } - } - - fun setInstance(instance: MiraiLogger.Factory) { - if (frozen.value) { - error( - "LoggerFactory instance had been frozen, so it's impossible to override it." + - "If you are using Mirai Console and you want to override platform logging implementation, " + - "please do so before initialization of MiraiConsole, that is, before `MiraiConsoleImplementation.start()`. " + - "Plugins are not allowed to override logging implementation, and this is done in the very fundamental implementation of Mirai Console so there is no way to escape that." + - "Normally it is only sensible for Mirai Console frontend implementor to do that." + - "If you are just using mirai-core, this error should not happen. There should be no limitation in overriding logging implementation with mirai-core. " + - "Check if you actually did use mirai-console somewhere, or please file an issue on https://github.com/mamoe/mirai/issues/new/choose" - ) - } - this._instance = instance - } - - inline fun wrapCurrent(mapper: (current: MiraiLogger.Factory) -> MiraiLogger.Factory) { - contract { callsInPlace(mapper, InvocationKind.EXACTLY_ONCE) } - setInstance(this.instance.let(mapper)) - } - - override fun create(requester: KClass<*>, identity: String?): MiraiLogger { - return instance.create(requester, identity) - } - - override fun create(requester: Class<*>, identity: String?): MiraiLogger { - return instance.create(requester, identity) - } - - override fun create(requester: KClass<*>): MiraiLogger { - return instance.create(requester) - } - - override fun create(requester: Class<*>): MiraiLogger { - return instance.create(requester) - } -} - -private class DefaultFactory : MiraiLogger.Factory { - @OptIn(MiraiInternalApi::class) - override fun create(requester: Class<*>, identity: String?): MiraiLogger { - return PlatformLogger(identity ?: requester.kotlin.simpleName ?: requester.simpleName) - } -} diff --git a/mirai-core-api/src/jvmBaseMain/kotlin/utils/Streamable.kt b/mirai-core-api/src/jvmBaseMain/kotlin/utils/Streamable.kt deleted file mode 100644 index 3f9c4cca81..0000000000 --- a/mirai-core-api/src/jvmBaseMain/kotlin/utils/Streamable.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2019-2022 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 - */ - -@file:JvmBlockingBridge - -package net.mamoe.mirai.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.toList -import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge -import net.mamoe.mirai.contact.announcement.Announcement -import net.mamoe.mirai.contact.announcement.Announcements -import net.mamoe.mirai.utils.JdkStreamSupport.toStream -import java.util.stream.Stream -import kotlin.coroutines.EmptyCoroutineContext - -/** - * 表示一个可以创建数据流 [Flow] 和 [Stream] 的对象. - * - * 实现这个接口的对象可以看做为元素 [T] 的集合. - * 例如 [Announcements] 可以看作是 [Announcement] 的集合, - * 使用 [Announcements.asFlow] 可以获取到包含所有 [Announcement] 列表的 [Flow], - * 使用 [Announcements.asStream] 可以获取到包含所有 [Announcement] 列表的 [Stream]. - * - * @since 2.13 - */ -public actual interface Streamable { - /** - * 创建一个能获取 [T] 的 [Flow]. - */ - public actual fun asFlow(): Flow - - /** - * 创建一个能获取该群内所有 [T] 的 [Stream]. - * - * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [asFlow]. - * - * 注: 为了资源的正确释放, 使用 [Stream] 时需要使用 `try-with-resource`. 如 - * - * ```java - * Streamable tmp; - * try (var stream = tmp.asStream()) { - * System.out.println(stream.findFirst()); - * } - * ``` - */ - public fun asStream(): Stream = asFlow().toStream( - context = if (this is CoroutineScope) this.coroutineContext else EmptyCoroutineContext, - ) - - /** - * 获取所有 [T] 列表, 将全部 [T] 都加载后再返回. - * - * @return 此时刻的 [T] 只读列表. - */ - public actual suspend fun toList(): List = asFlow().toList() -} \ No newline at end of file diff --git a/mirai-core-api/src/jvmBaseTest/kotlin/package.kt b/mirai-core-api/src/jvmBaseTest/kotlin/package.kt deleted file mode 100644 index 8bb3a229f5..0000000000 --- a/mirai-core-api/src/jvmBaseTest/kotlin/package.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2019-2022 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 \ No newline at end of file