[core] Remove jvmBase target (#2772)

* [core] Remove jvmBase: Contact, FileSupported, ExternalResource, FileCacheStrategy, RemoteFile

* [core] Remove jvmBase: AbsoluteFolder

* [core] Remove jvmBase: RoamingMessages

* [core] Remove jvmBase: EventChannel

* [core] Remove jvmBase: message

* [core] Remove jvmBase: logging

* [core] Remove jvmBase: Streamable

* [core] Remove jvmBase: AbstractBotConfiguration

* [core] Remove jvmBase: test and cleanup module
This commit is contained in:
Him188 2023-08-14 10:42:42 +08:00 committed by GitHub
parent 42763fd15c
commit 5ef291f648
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1691 additions and 3437 deletions

View File

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

View File

@ -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<Contact>
public suspend fun sendMessage(message: String): MessageReceipt<Contact> =
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 : Contact> C.sendImage(
imageStream: InputStream,
formatName: String? = null
): MessageReceipt<C> = imageStream.sendAsImageTo(this, formatName)
/**
* 将文件作为图片发送到指定联系人
* @param formatName 查看 [ExternalResource.formatName]
* @throws OverFileSizeMaxException
* @see FileCacheStrategy
*/
@JvmStatic
@JvmOverloads
public suspend fun <C : Contact> C.sendImage(
file: File,
formatName: String? = null
): MessageReceipt<C> = file.sendAsImageTo(this, formatName)
/**
* 将资源作为单独的图片消息发送给 [this]
*
* @see Contact.sendMessage 最终调用, 发送消息.
*/
@JvmStatic
public suspend fun <C : Contact> C.sendImage(resource: ExternalResource): MessageReceipt<C>
public suspend fun <C : Contact> C.sendImage(resource: ExternalResource): MessageReceipt<C> =
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)
}
}

View File

@ -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
/**
* 获取远程文件列表 (管理器).
*

View File

@ -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<AbsoluteFolder>
/**
* 获取该目录下所有子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. Kotlin 请使用 [folders].
*/
@JavaFriendlyAPI
public suspend fun foldersStream(): Stream<AbsoluteFolder>
/**
* 获取该目录下所有文件列表.
*/
public suspend fun files(): Flow<AbsoluteFile>
/**
* 获取该目录下所有文件列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. Kotlin 请使用 [files].
*/
@JavaFriendlyAPI
public suspend fun filesStream(): Stream<AbsoluteFile>
/**
* 获取该目录下所有文件和子目录列表.
*/
public suspend fun children(): Flow<AbsoluteFileFolder>
/**
* 获取该目录下所有文件和子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. Kotlin 请使用 [children].
*/
@JavaFriendlyAPI
public suspend fun childrenStream(): Stream<AbsoluteFileFolder>
///////////////////////////////////////////////////////////////////////////
// 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<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveFiles].
*/
@JavaFriendlyAPI
public suspend fun resolveFilesStream(
path: String
): Stream<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*/
@ -120,6 +162,16 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder {
path: String
): Flow<AbsoluteFileFolder>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveAll].
*/
@JavaFriendlyAPI
public suspend fun resolveAllStream(
path: String
): Stream<AbsoluteFileFolder>
/**
* 上传一个文件到该目录, 返回上传成功的文件标识.
*
@ -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 = "/"
}
}

View File

@ -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<MessageChain>
/**
* 查询所有漫游消息记录.
* 查询所有漫游消息记录. 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<MessageChain>
): Flow<MessageChain> = 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<MessageChain> = 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<MessageChain> = getMessagesStream(0, Long.MAX_VALUE, filter)
}

View File

@ -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<out BaseEvent : Event> @MiraiInternalApi public constructor(
baseEventClass: KClass<out BaseEvent>,
defaultCoroutineContext: CoroutineContext,
) {
public abstract class EventChannel<out BaseEvent : Event> @MiraiInternalApi public constructor(
public val baseEventClass: KClass<out BaseEvent>,
/**
* 此事件通道的默认 [CoroutineScope.coroutineContext]. 将会被添加给所有注册的事件监听器.
*/
public val defaultCoroutineContext: CoroutineContext
public val baseEventClass: KClass<out BaseEvent>
public val defaultCoroutineContext: CoroutineContext,
) {
/**
* 创建事件监听并将监听结果转发到 [channel]. [Channel.send] 抛出 [ClosedSendChannelException] 时停止 [Listener] 监听和转发.
*
@ -120,7 +112,16 @@ public expect abstract class EventChannel<out BaseEvent : Event> @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<out BaseEvent : Event> @MiraiInternalA
* @see filterIsInstance 过滤指定类型的事件
*/
@JvmSynthetic
public fun filter(filter: suspend (event: BaseEvent) -> Boolean): EventChannel<BaseEvent>
public fun filter(filter: suspend (event: BaseEvent) -> Boolean): EventChannel<BaseEvent> {
return FilterEventChannel(this, filter)
}
/**
* [EventChannel.filter] Java 版本.
@ -228,20 +231,32 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public fun filter(filter: (event: BaseEvent) -> Boolean): EventChannel<BaseEvent>
public fun filter(filter: (event: BaseEvent) -> Boolean): EventChannel<BaseEvent> {
return filter { runBIO { filter(it) } }
}
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
@JvmSynthetic
public inline fun <reified E : Event> filterIsInstance(): EventChannel<E>
public inline fun <reified E : Event> filterIsInstance(): EventChannel<E> =
filterIsInstance(E::class)
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
public fun <E : Event> filterIsInstance(kClass: KClass<out E>): EventChannel<E>
public fun <E : Event> filterIsInstance(kClass: KClass<out E>): EventChannel<E> {
return filter { kClass.isInstance(it) }.cast()
}
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
public fun <E : Event> filterIsInstance(clazz: Class<out E>): EventChannel<E> =
filterIsInstance(clazz.kotlin)
/**
@ -258,14 +273,30 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel<BaseEvent>
public fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel<BaseEvent> {
return context(coroutineExceptionHandler)
}
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
* @see context
*/
public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel<BaseEvent>
public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel<BaseEvent> {
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<Throwable>): EventChannel<BaseEvent> {
return exceptionHandler { coroutineExceptionHandler.accept(it) }
}
/**
* [coroutineScope] 作为这个 [EventChannel] 的父作用域.
@ -279,7 +310,9 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
*
* @see CoroutineScope.globalEventChannel `GlobalEventChannel.parentScope()` 的扩展
*/
public fun parentScope(coroutineScope: CoroutineScope): EventChannel<BaseEvent>
public fun parentScope(coroutineScope: CoroutineScope): EventChannel<BaseEvent> {
return context(coroutineScope.coroutineContext)
}
/**
* 指定协程父 [Job]. 之后在此 [EventChannel] 下创建的事件监听器都会成为 [job] 的子任务, [job] 被取消时, 所有的事件监听器都会被取消.
@ -289,7 +322,9 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
* @see parentScope
* @see context
*/
public fun parentJob(job: Job): EventChannel<BaseEvent>
public fun parentJob(job: Job): EventChannel<BaseEvent> {
return context(job)
}
// endregion
@ -390,7 +425,7 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
concurrency: ConcurrencyKind = LOCKED,
priority: EventPriority = EventPriority.NORMAL,
noinline handler: suspend E.(E) -> ListeningStatus,
): Listener<E>
): Listener<E> = subscribe(E::class, coroutineContext, concurrency, priority, handler)
/**
* [subscribe] 的区别是接受 [eventClass] 参数, 而不使用 `reified` 泛型. 通常推荐使用具体化类型参数.
@ -405,7 +440,10 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
concurrency: ConcurrencyKind = LOCKED,
priority: EventPriority = EventPriority.NORMAL,
handler: suspend E.(E) -> ListeningStatus,
): Listener<E>
): Listener<E> = subscribeInternal(
eventClass,
createListener0(coroutineContext, concurrency, priority) { it.handler(it); }
)
/**
* 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件.
@ -427,7 +465,7 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
noinline handler: suspend E.(E) -> Unit,
): Listener<E>
): Listener<E> = subscribeAlways(E::class, coroutineContext, concurrency, priority, handler)
/**
@ -441,7 +479,10 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: suspend E.(E) -> Unit,
): Listener<E>
): Listener<E> = subscribeInternal(
eventClass,
createListener0(coroutineContext, concurrency, priority) { it.handler(it); ListeningStatus.LISTENING }
)
/**
* 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件, 只监听一次.
@ -459,7 +500,7 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
coroutineContext: CoroutineContext = EmptyCoroutineContext,
priority: EventPriority = EventPriority.NORMAL,
noinline handler: suspend E.(E) -> Unit,
): Listener<E>
): Listener<E> = subscribeOnce(E::class, coroutineContext, priority, handler)
/**
* @see subscribeOnce
@ -469,7 +510,190 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
coroutineContext: CoroutineContext = EmptyCoroutineContext,
priority: EventPriority = EventPriority.NORMAL,
handler: suspend E.(E) -> Unit,
): Listener<E>
): Listener<E> = 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 <E : Event> subscribeAlways(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: Consumer<E>,
): Listener<E> = 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 <E : Event> subscribe(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: java.util.function.Function<E, ListeningStatus>,
): Listener<E> = 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 <E : Event> subscribeOnce(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: Consumer<E>,
): Listener<E> = 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<BaseEvent>(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<out BaseEvent> =
Channel<BaseEvent>(capacity).apply { forwardToChannel(this, coroutineContext, priority) }
// endregion
@ -481,7 +705,16 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
protected abstract fun <E : Event> registerListener(eventClass: KClass<out E>, listener: Listener<E>)
// to overcome visibility issue
internal fun <E : Event> registerListener0(eventClass: KClass<out E>, listener: Listener<E>)
@OptIn(MiraiInternalApi::class)
internal fun <E : Event> registerListener0(eventClass: KClass<out E>, listener: Listener<E>) {
return registerListener(eventClass, listener)
}
@OptIn(MiraiInternalApi::class)
private fun <L : Listener<E>, E : Event> subscribeInternal(eventClass: KClass<out E>, listener: L): L {
registerListener(eventClass, listener)
return listener
}
/**
* Creates [Listener] instance using the [listenerBlock] action.
@ -496,12 +729,13 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
): Listener<E>
// to overcome visibility issue
@OptIn(MiraiInternalApi::class)
internal fun <E : Event> createListener0(
coroutineContext: CoroutineContext,
concurrencyKind: ConcurrencyKind,
priority: EventPriority,
listenerBlock: suspend (E) -> ListeningStatus,
): Listener<E>
): Listener<E> = createListener(coroutineContext, concurrencyKind, priority, listenerBlock)
// endregion
}

View File

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

View File

@ -24,11 +24,111 @@ import net.mamoe.mirai.utils.MiraiInternalApi
* @see MessageSubscribersBuilder 查看上层 API
*/
@OptIn(MiraiInternalApi::class)
public expect abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal constructor(
public abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal constructor(
ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : CommonMessageSelectBuilderUnit<M, R>
) : CommonMessageSelectBuilderUnit<M, R>(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<M : MessageEvent, R> protec
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(result)
else -> ownerMessagePacket.subject.sendMessage(result.toString())
}
@ -204,6 +305,7 @@ public abstract class CommonMessageSelectBuilderUnit<M : MessageEvent, R> protec
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result)
else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString())
}

View File

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

View File

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

View File

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

View File

@ -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<Event> {
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<out Event>,
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<out Event>,
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<CancellableEvent>()?.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<out Event>,
coroutineContext,
annotation.concurrency,
annotation.priority
) {
callMethod(this)
}
}
ListeningStatus::class.java -> {
eventChannel.subscribe(
paramType.kotlin as KClass<out Event>,
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}")
}
}
}
}

View File

@ -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 <M : Any> SerializersModule.overwritePolymorphicWith(
internal fun <M : Any> SerializersModule.overwritePolymorphicWith(
type: KClass<M>,
serializer: KSerializer<M>
): 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<Any>) {
subclass(type, serializer)
}
}
})
}
//private inline fun <reified M : SingleMessage> SerializersModuleBuilder.hierarchicallyPolymorphic(serializer: KSerializer<M>) =
// hierarchicallyPolymorphic(M::class, serializer)

View File

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

View File

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

View File

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

View File

@ -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<Throwable?>,
public val exception: Deferred<Throwable?>,
) {
/**
* 撤回失败时的异常, 当撤回成功时为 `null`.
* 撤回产生的异常, 当撤回成功时为 `null`. Java [CompletableFuture] API.
*/
public val exception: Deferred<Throwable?>
public val exceptionFuture: CompletableFuture<Throwable?> by lazy { exception.asCompletableFuture() }
/**
* 撤回是否成功.
* 撤回是否成功. Kotlin [Deferred] API.
*/
public val isSuccess: Deferred<Boolean>
public val isSuccess: Deferred<Boolean> by lazy {
CompletableDeferred<Boolean>().apply {
exception.invokeOnCompletion {
complete(it == null)
}
}
}
/**
* 挂起协程直到撤回完成, 返回撤回时产生的异常. 当撤回成功时返回 `null`.
* 撤回是否成功. Java [CompletableFuture] API.
*/
public suspend fun awaitException(): Throwable?
public val isSuccessFuture: CompletableFuture<Boolean> by lazy { isSuccess.asCompletableFuture() }
/**
* 挂起协程直到撤回完成, 返回撤回的结果.
* 挂起协程 ( Java 为阻塞线程) 直到撤回完成, 返回撤回时产生的异常. 当撤回成功时返回 `null`.
*/
public suspend fun awaitIsSuccess(): Boolean
public suspend fun awaitException(): Throwable? {
return exception.await()
}
/**
* 挂起协程 ( Java 为阻塞线程) 直到撤回完成, 返回撤回的结果.
*/
public suspend fun awaitIsSuccess(): Boolean {
return isSuccess.await()
}
}

View File

@ -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 <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitFileMessage(this, data)
}
/**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/
public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage> {
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
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<FileMessage> // not polymorphic
public object Serializer :
KSerializer<FileMessage> by @OptIn(MiraiInternalApi::class) FallbackFileMessageSerializer()
}
@MiraiInternalApi

View File

@ -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<Message>.toMessageChain(): MessageChain = this.asSequence().toMessageChain()
// region delegate

View File

@ -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)
}
}
internal fun applyMppCopy(new: BotConfiguration)
///////////////////////////////////////////////////////////////////////////
// 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) {
new.workingDir = workingDir
new.cacheDir = cacheDir
}
}

View File

@ -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<Unit>
/**
* 打开 [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<ExternalResourceImplByFile>().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 <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C>
public suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
contact.uploadImage(this).sendTo(contact)
/**
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
*
* 注意本函数不会关闭流.
*
* @param formatName 查看 [ExternalResource.formatName]
* @throws OverFileSizeMaxException
*/
@JvmStatic
@JvmBlockingBridge
@JvmName("sendAsImage")
@JvmOverloads
public suspend fun <C : Contact> InputStream.sendAsImageTo(
contact: C,
formatName: String? = null,
): MessageReceipt<C> =
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 <C : Contact> File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt<C> {
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 <C : FileSupported> File.sendTo(
contact: C,
path: String,
callback: RemoteFile.ProgressionCallback? = null,
): MessageReceipt<C> = 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 <C : FileSupported> ExternalResource.sendAsFileTo(
contact: C,
path: String,
callback: RemoteFile.ProgressionCallback? = null,
): MessageReceipt<C> {
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
}
}

View File

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

View File

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

View File

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

View File

@ -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 open fun create(requester: KClass<*>): MiraiLogger
public fun create(requester: KClass<*>): MiraiLogger = create(requester, null)
public companion object INSTANCE : Factory
/**
* 创建 [MiraiLogger] 实例.
*
* @param requester 请求创建 [MiraiLogger] 的对象
*/
public fun create(requester: Class<*>): MiraiLogger = create(requester, null)
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 <T : MiraiLogger> plus(follower: T): T = follower
}
public inline fun MiraiLogger.verbose(message: () -> String) {
if (isVerboseEnabled) verbose(message())

View File

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

View File

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

View File

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

View File

@ -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<T> {
public interface Streamable<T> {
/**
* 创建一个能获取 [T] [Flow].
*/
public fun asFlow(): Flow<T>
/**
* 创建一个能获取该群内所有 [T] [Stream].
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. Kotlin 请使用 [asFlow].
*
* : 为了资源的正确释放, 使用 [Stream] 时需要使用 `try-with-resource`.
*
* ```java
* Streamable<String> tmp;
* try (var stream = tmp.asStream()) {
* System.out.println(stream.findFirst());
* }
* ```
*/
public fun asStream(): Stream<T> = asFlow().toStream(
context = if (this is CoroutineScope) this.coroutineContext else EmptyCoroutineContext,
)
/**
* 获取所有 [T] 列表, 将全部 [T] 都加载后再返回.
*
* @return 此时刻的 [T] 只读列表.
*/
public open suspend fun toList(): List<T>
public suspend fun toList(): List<T> = asFlow().toList()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Contact>
/**
* 发送纯文本消息
* @see sendMessage
*/
public actual suspend fun sendMessage(message: String): MessageReceipt<Contact> =
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 : Contact> C.sendImage(
imageStream: InputStream,
formatName: String? = null
): MessageReceipt<C> = imageStream.sendAsImageTo(this, formatName)
/**
* 将文件作为图片发送到指定联系人
* @param formatName 查看 [ExternalResource.formatName]
* @throws OverFileSizeMaxException
* @see FileCacheStrategy
*/
@JvmStatic
@JvmOverloads
public suspend fun <C : Contact> C.sendImage(
file: File,
formatName: String? = null
): MessageReceipt<C> = file.sendAsImageTo(this, formatName)
/**
* 将资源作为单独的图片消息发送给 [this]
*
* @see Contact.sendMessage 最终调用, 发送消息.
*/
@JvmStatic
public actual suspend fun <C : Contact> C.sendImage(resource: ExternalResource): MessageReceipt<C> =
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)
}
}

View File

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

View File

@ -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<AbsoluteFolder>
/**
* 获取该目录下所有子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. Kotlin 请使用 [folders].
*/
@JavaFriendlyAPI
public suspend fun foldersStream(): Stream<AbsoluteFolder>
/**
* 获取该目录下所有文件列表.
*/
public actual suspend fun files(): Flow<AbsoluteFile>
/**
* 获取该目录下所有文件列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. Kotlin 请使用 [files].
*/
@JavaFriendlyAPI
public suspend fun filesStream(): Stream<AbsoluteFile>
/**
* 获取该目录下所有文件和子目录列表.
*/
public actual suspend fun children(): Flow<AbsoluteFileFolder>
/**
* 获取该目录下所有文件和子目录列表.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. Kotlin 请使用 [children].
*/
@JavaFriendlyAPI
public suspend fun childrenStream(): Stream<AbsoluteFileFolder>
///////////////////////////////////////////////////////////////////////////
// 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<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveFiles].
*/
@JavaFriendlyAPI
public suspend fun resolveFilesStream(
path: String
): Stream<AbsoluteFile>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*/
public actual suspend fun resolveAll(
path: String
): Flow<AbsoluteFileFolder>
/**
* 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录.
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. Kotlin 请使用 [resolveAll].
*/
@JavaFriendlyAPI
public suspend fun resolveAllStream(
path: String
): Stream<AbsoluteFileFolder>
/**
* 上传一个文件到该目录, 返回上传成功的文件标识.
*
* 会在必要时尝试创建远程目录.
*
* ### [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<AbsoluteFile, Long>? = null,
): AbsoluteFile
public actual companion object {
/**
* 根目录 folder ID.
* @see id
*/
public actual const val ROOT_FOLDER_ID: String = "/"
}
}

View File

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

View File

@ -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<MessageChain>
/**
* 查询所有漫游消息记录. 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<MessageChain> = 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<MessageChain> = 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<MessageChain> = getMessagesStream(0, Long.MAX_VALUE, filter)
}

View File

@ -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<out BaseEvent : Event> @MiraiInternalApi public actual constructor(
public actual val baseEventClass: KClass<out BaseEvent>,
/**
* 此事件通道的默认 [CoroutineScope.coroutineContext]. 将会被添加给所有注册的事件监听器.
*/
public actual val defaultCoroutineContext: CoroutineContext,
) {
/**
* 创建事件监听并将监听结果转发到 [channel]. [Channel.send] 抛出 [ClosedSendChannelException] 时停止 [Listener] 监听和转发.
*
* 返回创建的会转发监听到的所有事件到 [channel] [事件监听器][Listener]. [停止][Listener.complete] 该监听器会停止转发, 不会影响目标 [channel].
*
* [Channel.send] 挂起, 则监听器也会挂起, 也就可能会导致事件广播过程挂起.
*
* 示例:
*
* ```
* val eventChannel: EventChannel<BotEvent> = ...
* val channel = Channel<BotEvent>() // 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<BotEvent> = ...
* val flow: Flow<BotEvent> = eventChannel.asFlow()
*
* flow.collect { // it
* //
* }
*
* flow.filterIsInstance<GroupMessageEvent>.collect { // it: GroupMessageEvent
* // 处理事件 ...
* }
*
* flow.filterIsInstance<FriendMessageEvent>.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<BaseEvent>
// region transforming operations
/**
* 添加一个过滤器. 过滤器将在收到任何事件之后, 传递给通过 [EventChannel.subscribe] 注册的监听器之前调用.
*
* [filter] 返回 `true`, 该事件将会被传给监听器. 否则将会被忽略, **监听器继续监听**.
*
* ## 线性顺序
* 多个 [filter] 的处理是线性且有顺序的. 若一个 [filter] 已经返回了 `false` (代表忽略这个事件), 则会立即忽略, 而不会传递给后续过滤器.
*
* 示例:
* ```
* GlobalEventChannel // GlobalEventChannel 会收到全局所有事件, 事件类型是 Event
* .filterIsInstance<BotEvent>() // 过滤, 只接受 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<BaseEvent> {
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<BaseEvent> {
return filter { runBIO { filter(it) } }
}
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
@JvmSynthetic
public actual inline fun <reified E : Event> filterIsInstance(): EventChannel<E> =
filterIsInstance(E::class)
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
public actual fun <E : Event> filterIsInstance(kClass: KClass<out E>): EventChannel<E> {
return filter { kClass.isInstance(it) }.cast()
}
/**
* 过滤事件的类型. 返回一个只包含 [E] 类型事件的 [EventChannel]
* @see filter 获取更多信息
*/
public fun <E : Event> filterIsInstance(clazz: Class<out E>): EventChannel<E> =
filterIsInstance(clazz.kotlin)
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineContexts].
* [coroutineContexts] 会覆盖 [defaultCoroutineContext] 中的重复元素.
*
* 此操作不会修改 [`this.coroutineContext`][defaultCoroutineContext], 只会创建一个新的 [EventChannel].
*/
public actual abstract fun context(vararg coroutineContexts: CoroutineContext): EventChannel<BaseEvent>
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [this.coroutineContext][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
* @see context
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public actual fun exceptionHandler(coroutineExceptionHandler: CoroutineExceptionHandler): EventChannel<BaseEvent> {
return context(coroutineExceptionHandler)
}
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
* @see context
*/
public actual fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel<BaseEvent> {
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<Throwable>): EventChannel<BaseEvent> {
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<BaseEvent> {
return context(coroutineScope.coroutineContext)
}
/**
* 指定协程父 [Job]. 之后在此 [EventChannel] 下创建的事件监听器都会成为 [job] 的子任务, [job] 被取消时, 所有的事件监听器都会被取消.
*
* 注意: 监听器不会失败 ([Job.cancel]). 监听器处理过程的异常都会被捕获然后交由 [CoroutineExceptionHandler] 处理, 因此 [job] 不会因为子任务监听器的失败而被取消.
*
* @see parentScope
* @see context
*/
public actual fun parentJob(job: Job): EventChannel<BaseEvent> {
return context(job)
}
// endregion
// region subscribe
/**
* 创建一个事件监听器, 监听事件通道中所有 [E] 及其子类事件.
*
* 每当 [事件广播][Event.broadcast] , [handler] 都会被执行.
*
*
* ## 创建监听
* 调用本函数:
* ```
* eventChannel.subscribe<E> { /* 会收到此通道中的所有是 E 的事件 */ }
* ```
*
* ## 生命周期
*
* ### 通过协程作用域管理监听器
* 本函数将会创建一个 [Job], 成为 [parentJob] 中的子任务. 可创建一个 [CoroutineScope] 来管理所有的监听器:
* ```
* val scope = CoroutineScope(SupervisorJob())
*
* val scopedChannel = eventChannel.parentScope(scope) // 将协程作用域 scope 附加到这个 EventChannel
*
* scopedChannel.subscribeAlways<MemberJoinEvent> { /* ... */ } // 启动监听, 监听器协程会作为 scope 的子任务
* scopedChannel.subscribeAlways<MemberMuteEvent> { /* ... */ } // 启动监听, 监听器协程会作为 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 <reified E : Event> subscribe(
coroutineContext: CoroutineContext,
concurrency: ConcurrencyKind,
priority: EventPriority,
noinline handler: suspend E.(E) -> ListeningStatus,
): Listener<E> = subscribe(E::class, coroutineContext, concurrency, priority, handler)
/**
* [subscribe] 的区别是接受 [eventClass] 参数, 而不使用 `reified` 泛型. 通常推荐使用具体化类型参数.
*
* @return 监听器实例. 此监听器已经注册到指定事件上, 在事件广播时将会调用 [handler]
* @see subscribe
*/
@JvmSynthetic
public actual fun <E : Event> subscribe(
eventClass: KClass<out E>,
coroutineContext: CoroutineContext,
concurrency: ConcurrencyKind,
priority: EventPriority,
handler: suspend E.(E) -> ListeningStatus,
): Listener<E> = 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 <reified E : Event> subscribeAlways(
coroutineContext: CoroutineContext,
concurrency: ConcurrencyKind,
priority: EventPriority,
noinline handler: suspend E.(E) -> Unit,
): Listener<E> = subscribeAlways(E::class, coroutineContext, concurrency, priority, handler)
/**
* @see subscribe
* @see subscribeAlways
*/
@JvmSynthetic
public actual fun <E : Event> subscribeAlways(
eventClass: KClass<out E>,
coroutineContext: CoroutineContext,
concurrency: ConcurrencyKind,
priority: EventPriority,
handler: suspend E.(E) -> Unit,
): Listener<E> = 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 <reified E : Event> subscribeOnce(
coroutineContext: CoroutineContext,
priority: EventPriority,
noinline handler: suspend E.(E) -> Unit,
): Listener<E> = subscribeOnce(E::class, coroutineContext, priority, handler)
/**
* @see subscribeOnce
*/
public actual fun <E : Event> subscribeOnce(
eventClass: KClass<out E>,
coroutineContext: CoroutineContext,
priority: EventPriority,
handler: suspend E.(E) -> Unit,
): Listener<E> = 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 <E : Event> subscribeAlways(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: Consumer<E>,
): Listener<E> = 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 <E : Event> subscribe(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: java.util.function.Function<E, ListeningStatus>,
): Listener<E> = 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 <E : Event> subscribeOnce(
eventClass: Class<out E>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
concurrency: ConcurrencyKind = CONCURRENT,
priority: EventPriority = EventPriority.NORMAL,
handler: Consumer<E>,
): Listener<E> = 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<BaseEvent>(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<out BaseEvent> =
Channel<BaseEvent>(capacity).apply { forwardToChannel(this, coroutineContext, priority) }
// endregion
// region impl
// protected, to hide from users
@MiraiInternalApi
protected actual abstract fun <E : Event> registerListener(eventClass: KClass<out E>, listener: Listener<E>)
// to overcome visibility issue
@OptIn(MiraiInternalApi::class)
internal actual fun <E : Event> registerListener0(eventClass: KClass<out E>, listener: Listener<E>) {
return registerListener(eventClass, listener)
}
@OptIn(MiraiInternalApi::class)
private fun <L : Listener<E>, E : Event> subscribeInternal(eventClass: KClass<out E>, 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 <E : Event> createListener(
coroutineContext: CoroutineContext,
concurrencyKind: ConcurrencyKind,
priority: EventPriority,
listenerBlock: suspend (E) -> ListeningStatus,
): Listener<E>
// to overcome visibility issue
@OptIn(MiraiInternalApi::class)
internal actual fun <E : Event> createListener0(
coroutineContext: CoroutineContext,
concurrencyKind: ConcurrencyKind,
priority: EventPriority,
listenerBlock: suspend (E) -> ListeningStatus,
): Listener<E> = createListener(coroutineContext, concurrencyKind, priority, listenerBlock)
// endregion
}

View File

@ -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<M : MessageEvent, R> @PublishedApi internal actual constructor(
ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : CommonMessageSelectBuilderUnit<M, R>(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
}
}

View File

@ -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<Event> {
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<out Event>,
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<out Event>,
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<CancellableEvent>()?.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<out Event>,
coroutineContext,
annotation.concurrency,
annotation.priority
) {
callMethod(this)
}
}
ListeningStatus::class.java -> {
eventChannel.subscribe(
paramType.kotlin as KClass<out Event>,
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}")
}
}
}

View File

@ -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 <M : Any> SerializersModule.overwritePolymorphicWith(
type: KClass<M>,
serializer: KSerializer<M>
): 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<Any>) {
subclass(type, serializer)
}
}
})
}

View File

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

View File

@ -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<Throwable?>,
) {
/**
* 撤回时产生的异常, 当撤回成功时为 `null`. Java [CompletableFuture] API.
*/
public val exceptionFuture: CompletableFuture<Throwable?> by lazy { exception.asCompletableFuture() }
/**
* 撤回是否成功. Kotlin [Deferred] API.
*/
public actual val isSuccess: Deferred<Boolean> by lazy {
CompletableDeferred<Boolean>().apply {
exception.invokeOnCompletion {
complete(it == null)
}
}
}
/**
* 撤回是否成功. Java [CompletableFuture] API.
*/
public val isSuccessFuture: CompletableFuture<Boolean> 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()
}
}

View File

@ -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 <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitFileMessage(this, data)
}
/**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/
public actual companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
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<FileMessage> by @OptIn(MiraiInternalApi::class) FallbackFileMessageSerializer()
}

View File

@ -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<Message>.toMessageChain(): MessageChain = this.asSequence().toMessageChain()

View File

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

View File

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

View File

@ -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<Unit>
/**
* 打开 [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<ExternalResourceImplByFile>().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 <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
contact.uploadImage(this).sendTo(contact)
/**
* 读取 [InputStream] 到临时文件并将其作为图片发送到指定联系人.
*
* 注意本函数不会关闭流.
*
* @param formatName 查看 [ExternalResource.formatName]
* @throws OverFileSizeMaxException
*/
@JvmStatic
@JvmBlockingBridge
@JvmName("sendAsImage")
@JvmOverloads
public suspend fun <C : Contact> InputStream.sendAsImageTo(
contact: C,
formatName: String? = null,
): MessageReceipt<C> =
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 <C : Contact> File.sendAsImageTo(contact: C, formatName: String? = null): MessageReceipt<C> {
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 <C : FileSupported> File.sendTo(
contact: C,
path: String,
callback: RemoteFile.ProgressionCallback? = null,
): MessageReceipt<C> = 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 <C : FileSupported> ExternalResource.sendAsFileTo(
contact: C,
path: String,
callback: RemoteFile.ProgressionCallback? = null,
): MessageReceipt<C> {
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
}
}

View File

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

View File

@ -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 <T : MiraiLogger> 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)
}
}

View File

@ -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> {
/**
* 创建一个能获取 [T] [Flow].
*/
public actual fun asFlow(): Flow<T>
/**
* 创建一个能获取该群内所有 [T] [Stream].
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. Kotlin 请使用 [asFlow].
*
* : 为了资源的正确释放, 使用 [Stream] 时需要使用 `try-with-resource`.
*
* ```java
* Streamable<String> tmp;
* try (var stream = tmp.asStream()) {
* System.out.println(stream.findFirst());
* }
* ```
*/
public fun asStream(): Stream<T> = asFlow().toStream(
context = if (this is CoroutineScope) this.coroutineContext else EmptyCoroutineContext,
)
/**
* 获取所有 [T] 列表, 将全部 [T] 都加载后再返回.
*
* @return 此时刻的 [T] 只读列表.
*/
public actual suspend fun toList(): List<T> = asFlow().toList()
}

View File

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