Commonize projects: mirai-core series, and mirai-console-compiler-annotations

This commit is contained in:
Him188 2022-05-17 10:21:08 +01:00
parent d96641dedb
commit 0571be1a55
119 changed files with 6137 additions and 1583 deletions

View File

@ -38,7 +38,7 @@ object Versions {
const val io = "0.1.16" const val io = "0.1.16"
const val coroutinesIo = "0.1.16" const val coroutinesIo = "0.1.16"
const val blockingBridge = "2.0.0-162.1" const val blockingBridge = "2.1.0-162.1"
const val dynamicDelegation = "0.3.0-162.4" const val dynamicDelegation = "0.3.0-162.4"
const val androidGradlePlugin = "4.1.1" const val androidGradlePlugin = "4.1.1"
@ -58,7 +58,7 @@ object Versions {
const val junit = "5.7.2" const val junit = "5.7.2"
const val yamlkt = "0.11.0" const val yamlkt = "0.12.0"
const val intellijGradlePlugin = "1.5.3" const val intellijGradlePlugin = "1.5.3"
// const val kotlinIntellijPlugin = "211-1.5.20-release-284-IJ7442.40" // keep to newest as kotlinCompiler // const val kotlinIntellijPlugin = "211-1.5.20-release-284-IJ7442.40" // keep to newest as kotlinCompiler

View File

@ -9,13 +9,13 @@
package net.mamoe.mirai.console.compiler.common package net.mamoe.mirai.console.compiler.common
import org.intellij.lang.annotations.Language import kotlin.jvm.JvmField
/** /**
* @suppress 这是内部 API. 可能在任意时刻变动 * @suppress 这是内部 API. 可能在任意时刻变动
*/ */
public object CheckerConstants { public object CheckerConstants {
@Language("RegExp") // @Language("RegExp")
public const val PLUGIN_ID_PATTERN: String = """([a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)\.([a-zA-Z]\w*(?:-\w+)*)""" public const val PLUGIN_ID_PATTERN: String = """([a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)\.([a-zA-Z]\w*(?:-\w+)*)"""
@JvmField @JvmField

View File

@ -0,0 +1,10 @@
/*
* 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,17 +1,16 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.internal.utils.isSliderCaptchaSupportKind
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default import net.mamoe.mirai.utils.LoginSolver.Companion.Default
@ -38,7 +37,7 @@ public actual abstract class LoginSolver public actual constructor() {
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败 * 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/ */
public actual open val isSliderCaptchaSupported: Boolean public actual open val isSliderCaptchaSupported: Boolean
get() = isSliderCaptchaSupportKind ?: true get() = System.getProperty("mirai.slider.captcha.supported") != null
/** /**
* 处理滑动验证码. * 处理滑动验证码.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -21,9 +21,11 @@ import net.mamoe.mirai.message.action.BotNudge
import net.mamoe.mirai.message.action.MemberNudge import net.mamoe.mirai.message.action.MemberNudge
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.BotConfiguration import net.mamoe.mirai.utils.BotConfiguration
import net.mamoe.mirai.utils.ConcurrentHashMap
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.NotStableForInheritance
import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.JvmStatic
import kotlin.jvm.JvmSynthetic
/** /**
* 登录, 返回 [this] * 登录, 返回 [this]
@ -180,7 +182,7 @@ public interface Bot : CoroutineScope, ContactOrBot, UserOrBot {
public companion object { public companion object {
@Suppress("ObjectPropertyName") @Suppress("ObjectPropertyName")
internal val _instances: ConcurrentHashMap<Long, Bot> = ConcurrentHashMap() internal val _instances: MutableMap<Long, Bot> = ConcurrentHashMap()
/** /**
* 复制一份此时的 [Bot] 实例列表. * 复制一份此时的 [Bot] 实例列表.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -7,19 +7,15 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmBlockingBridge
@file:Suppress("INAPPLICABLE_JVM_NAME") @file:Suppress("INAPPLICABLE_JVM_NAME")
package net.mamoe.mirai.contact.announcement package net.mamoe.mirai.contact.announcement
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.PermissionDeniedException import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.NotStableForInheritance
import java.util.stream.Stream
/** /**
@ -44,7 +40,7 @@ import java.util.stream.Stream
* @since 2.7 * @since 2.7
*/ */
@NotStableForInheritance @NotStableForInheritance
public interface Announcements { public expect interface Announcements {
/** /**
* 创建一个能获取该群内所有群公告列表的 [Flow]. [Flow] 被使用时才会分页下载 [OnlineAnnouncement]. * 创建一个能获取该群内所有群公告列表的 [Flow]. [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
* *
@ -52,15 +48,6 @@ public interface Announcements {
*/ */
public suspend fun asFlow(): Flow<OnlineAnnouncement> public suspend fun asFlow(): Flow<OnlineAnnouncement>
/**
* 创建一个能获取该群内所有群公告列表的 [Stream]. [Stream] 被使用时才会分页下载 [OnlineAnnouncement].
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] [Stream] [收集][Stream.collect].
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. Kotlin 请使用 [asFlow].
*/
public fun asStream(): Stream<OnlineAnnouncement>
/** /**
* 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回. * 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
* *
@ -68,7 +55,7 @@ public interface Announcements {
* *
* @return 此时刻的群公告只读列表. * @return 此时刻的群公告只读列表.
*/ */
public suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList() public open suspend fun toList(): List<OnlineAnnouncement>
/** /**

View File

@ -13,6 +13,7 @@ import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor import net.mamoe.mirai.message.data.visitor.RecursiveMessageVisitor
import net.mamoe.mirai.message.data.visitor.accept import net.mamoe.mirai.message.data.visitor.accept
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.isSameType
/** /**
* One after one, hierarchically. * One after one, hierarchically.
@ -166,9 +167,7 @@ public class CombinedMessage @MessageChainConstructor constructor(
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (!isSameType(this, other)) return false
other as CombinedMessage
if (element != other.element) return false if (element != other.element) return false
if (tail != other.tail) return false if (tail != other.tail) return false

View File

@ -41,6 +41,10 @@ import net.mamoe.mirai.message.data.Image.Key.queryUrl
import net.mamoe.mirai.message.data.visitor.MessageVisitor import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
import kotlin.jvm.JvmSynthetic
/** /**
* 自定义表情 (收藏的表情) 和普通图片. * 自定义表情 (收藏的表情) 和普通图片.
@ -396,7 +400,7 @@ public interface Image : Message, MessageContent, CodableMessage {
* @see IMirai.createImage * @see IMirai.createImage
*/ */
@JvmSynthetic @JvmSynthetic
public inline fun Image(imageId: String): Image = Image.Builder.newBuilder(imageId).build() public inline fun Image(imageId: String): Image = Builder.newBuilder(imageId).build()
/** /**
* 使用 [Image.Builder] 构建一个 [Image]. * 使用 [Image.Builder] 构建一个 [Image].
@ -405,8 +409,8 @@ public inline fun Image(imageId: String): Image = Image.Builder.newBuilder(image
* @since 2.9.0 * @since 2.9.0
*/ */
@JvmSynthetic @JvmSynthetic
public inline fun Image(imageId: String, builderAction: Image.Builder.() -> Unit = {}): Image = public inline fun Image(imageId: String, builderAction: Builder.() -> Unit = {}): Image =
Image.Builder.newBuilder(imageId).apply(builderAction).build() Builder.newBuilder(imageId).apply(builderAction).build()
public enum class ImageType( public enum class ImageType(
/** /**

View File

@ -19,7 +19,6 @@ import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_1
import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2 import net.mamoe.mirai.message.data.Image.Key.IMAGE_RESOURCE_ID_REGEX_2
import net.mamoe.mirai.message.data.visitor.MessageVisitor import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.asImmutable
import net.mamoe.mirai.utils.castOrNull import net.mamoe.mirai.utils.castOrNull
import net.mamoe.mirai.utils.replaceAllKotlin import net.mamoe.mirai.utils.replaceAllKotlin

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.network package net.mamoe.mirai.network
@ -13,6 +13,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.utils.DeprecatedSinceMirai import net.mamoe.mirai.utils.DeprecatedSinceMirai
import kotlin.jvm.JvmOverloads
/** /**
* [Bot] 被迫下线时抛出, 作为 [Job.cancel] `cause` * [Bot] 被迫下线时抛出, 作为 [Job.cancel] `cause`

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -9,8 +9,10 @@
package net.mamoe.mirai.spi package net.mamoe.mirai.spi
import kotlinx.io.errors.IOException
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import java.io.IOException import kotlin.coroutines.cancellation.CancellationException
import kotlin.jvm.JvmStatic
/** /**
* 将源音频文件转换为 silk v3 with tencent 格式 * 将源音频文件转换为 silk v3 with tencent 格式
@ -33,17 +35,17 @@ public interface AudioToSilkService : BaseService {
* @see [runAutoClose] * @see [runAutoClose]
* @see [useAutoClose] * @see [useAutoClose]
*/ */
@Throws(IOException::class) @Throws(IOException::class, CancellationException::class)
public suspend fun convert(source: ExternalResource): ExternalResource public suspend fun convert(source: ExternalResource): ExternalResource
@MiraiExperimentalApi @MiraiExperimentalApi
public companion object : AudioToSilkService { public companion object : AudioToSilkService {
private val loader = SPIServiceLoader(object : AudioToSilkService { private val loader = SPIServiceLoader(object : AudioToSilkService {
override suspend fun convert(source: ExternalResource): ExternalResource = source override suspend fun convert(source: ExternalResource): ExternalResource = source
}, AudioToSilkService::class.java) }, AudioToSilkService::class)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
@Throws(IOException::class) @Throws(IOException::class, CancellationException::class)
override suspend fun convert(source: ExternalResource): ExternalResource { override suspend fun convert(source: ExternalResource): ExternalResource {
return loader.service.convert(source) return loader.service.convert(source)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -11,7 +11,8 @@ package net.mamoe.mirai.spi
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import java.util.* import kotlin.jvm.JvmField
import kotlin.reflect.KClass
/** /**
* 基本 SPI 接口 * 基本 SPI 接口
@ -23,25 +24,16 @@ public interface BaseService {
public val priority: Int get() = 5 public val priority: Int get() = 5
} }
internal class SPIServiceLoader<T : BaseService>( internal expect class SPIServiceLoader<T : BaseService>(
@JvmField val defaultService: T, defaultService: T,
@JvmField val serviceType: Class<T>, serviceType: KClass<T>,
) { ) {
@JvmField @JvmField
var service: T = defaultService var service: T
fun reload() { fun reload()
val loader = ServiceLoader.load(serviceType)
service = loader.minByOrNull { it.priority } ?: defaultService
}
init {
reload()
}
companion object { companion object {
val SPI_SERVICE_LOADER_LOGGER by lazy { val SPI_SERVICE_LOADER_LOGGER: MiraiLogger
MiraiLogger.Factory.create(SPIServiceLoader::class.java, "spi-service-loader")
}
} }
} }

View File

@ -17,17 +17,13 @@ package net.mamoe.mirai.utils
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.Json
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotFactory import net.mamoe.mirai.BotFactory
import net.mamoe.mirai.event.events.BotOfflineEvent import net.mamoe.mirai.event.events.BotOfflineEvent
import java.io.File
import java.io.InputStream
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import kotlin.jvm.*
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/** /**
* [Bot] 配置. 用于 [BotFactory.newBot] * [Bot] 配置. 用于 [BotFactory.newBot]
@ -53,18 +49,13 @@ import kotlin.time.Duration.Companion.milliseconds
* ``` * ```
*/ */
@Suppress("PropertyName") @Suppress("PropertyName")
public open class BotConfiguration { // open for Java public expect open class BotConfiguration() { // open for Java
/**
* 工作目录. 默认为 "."
*/
public var workingDir: File = File(".")
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Coroutines // Coroutines
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
/** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */ /** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
public var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext public var parentCoroutineContext: CoroutineContext
/** /**
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext]. * 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
@ -120,9 +111,7 @@ public open class BotConfiguration { // open for Java
*/ */
@JvmSynthetic @JvmSynthetic
@ConfigurationDsl @ConfigurationDsl
public suspend inline fun inheritCoroutineContext() { public suspend inline fun inheritCoroutineContext()
parentCoroutineContext = coroutineContext
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -130,7 +119,7 @@ public open class BotConfiguration { // open for Java
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
/** 连接心跳包周期. 过长会导致被服务器断开连接. */ /** 连接心跳包周期. 过长会导致被服务器断开连接. */
public var heartbeatPeriodMillis: Long = 60.secondsToMillis public var heartbeatPeriodMillis: Long
/** /**
* 状态心跳包周期. 过长会导致掉线. * 状态心跳包周期. 过长会导致掉线.
@ -138,13 +127,13 @@ public open class BotConfiguration { // open for Java
* @since 2.6 * @since 2.6
* @see heartbeatStrategy * @see heartbeatStrategy
*/ */
public var statHeartbeatPeriodMillis: Long = 300.secondsToMillis public var statHeartbeatPeriodMillis: Long
/** /**
* 心跳策略. * 心跳策略.
* @since 2.6.3 * @since 2.6.3
*/ */
public var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB public var heartbeatStrategy: HeartbeatStrategy
/** /**
* 心跳策略. * 心跳策略.
@ -178,7 +167,7 @@ public open class BotConfiguration { // open for Java
* 每次心跳时等待结果的时间. * 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响. * 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/ */
public var heartbeatTimeoutMillis: Long = 5.secondsToMillis public var heartbeatTimeoutMillis: Long
/** 心跳失败后的第一次重连前的等待时间. */ /** 心跳失败后的第一次重连前的等待时间. */
@Deprecated( @Deprecated(
@ -186,7 +175,7 @@ public open class BotConfiguration { // open for Java
level = DeprecationLevel.HIDDEN level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8 ) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public var firstReconnectDelayMillis: Long = 5.secondsToMillis public var firstReconnectDelayMillis: Long
/** 重连失败后, 继续尝试的每次等待时间 */ /** 重连失败后, 继续尝试的每次等待时间 */
@Deprecated( @Deprecated(
@ -194,10 +183,10 @@ public open class BotConfiguration { // open for Java
level = DeprecationLevel.HIDDEN level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8 ) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public var reconnectPeriodMillis: Long = 5.secondsToMillis public var reconnectPeriodMillis: Long
/** 最多尝试多少次重连 */ /** 最多尝试多少次重连 */
public var reconnectionRetryTimes: Int = Int.MAX_VALUE public var reconnectionRetryTimes: Int
/** /**
* 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`. * 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
@ -206,7 +195,7 @@ public open class BotConfiguration { // open for Java
* *
* @since 2.1 * @since 2.1
*/ */
public var autoReconnectOnForceOffline: Boolean = false public var autoReconnectOnForceOffline: Boolean
/** /**
* 验证码处理器 * 验证码处理器
@ -218,10 +207,10 @@ public open class BotConfiguration { // open for Java
* *
* @see LoginSolver * @see LoginSolver
*/ */
public var loginSolver: LoginSolver? = LoginSolver.Default public var loginSolver: LoginSolver?
/** 使用协议类型 */ /** 使用协议类型 */
public var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE public var protocol: MiraiProtocol
public enum class MiraiProtocol { public enum class MiraiProtocol {
/** /**
@ -265,23 +254,21 @@ public open class BotConfiguration { // open for Java
* *
* @since 2.2 * @since 2.2
*/ */
public var highwayUploadCoroutineCount: Int = Runtime.getRuntime().availableProcessors() public var highwayUploadCoroutineCount: Int
/** /**
* 设置 [autoReconnectOnForceOffline] `true`, 即在被挤下线时自动重连. * 设置 [autoReconnectOnForceOffline] `true`, 即在被挤下线时自动重连.
* @since 2.1 * @since 2.1
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun autoReconnectOnForceOffline() { public fun autoReconnectOnForceOffline()
autoReconnectOnForceOffline = true
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Device // Device
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@JvmField @JvmField
internal var accountSecrets: Boolean = true internal var accountSecrets: Boolean
/** /**
* 禁止保存 `account.secrets`. * 禁止保存 `account.secrets`.
@ -291,16 +278,14 @@ public open class BotConfiguration { // open for Java
* *
* @since 2.11 * @since 2.11
*/ */
public fun disableAccountSecretes() { public fun disableAccountSecretes()
accountSecrets = false
}
/** /**
* 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息. * 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
* @see fileBasedDeviceInfo 使用指定文件存储设备信息 * @see fileBasedDeviceInfo 使用指定文件存储设备信息
* @see randomDeviceInfo 使用随机设备信息 * @see randomDeviceInfo 使用随机设备信息
*/ */
public var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually. public var deviceInfo: ((Bot) -> DeviceInfo)?
/** /**
* 使用随机设备信息. * 使用随机设备信息.
@ -308,9 +293,7 @@ public open class BotConfiguration { // open for Java
* @see deviceInfo * @see deviceInfo
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun randomDeviceInfo() { public fun randomDeviceInfo()
deviceInfo = null
}
/** /**
* 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息 * 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
@ -318,11 +301,7 @@ public open class BotConfiguration { // open for Java
* @see deviceInfo * @see deviceInfo
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun loadDeviceInfoJson(json: String) { public fun loadDeviceInfoJson(json: String)
deviceInfo = {
Companion.json.decodeFromString(DeviceInfo.serializer(), json)
}
}
/** /**
* 使用文件存储设备信息. * 使用文件存储设备信息.
@ -333,9 +312,7 @@ public open class BotConfiguration { // open for Java
*/ */
@JvmOverloads @JvmOverloads
@ConfigurationDsl @ConfigurationDsl
public fun fileBasedDeviceInfo(filepath: String = "device.json") { public fun fileBasedDeviceInfo(filepath: String = "device.json")
deviceInfo = getFileBasedDeviceInfoSupplier { workingDir.resolve(filepath) }
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Logging // Logging
@ -351,9 +328,7 @@ public open class BotConfiguration { // open for Java
* *
* @see MiraiLogger * @see MiraiLogger
*/ */
public var botLoggerSupplier: ((Bot) -> MiraiLogger) = { public var botLoggerSupplier: ((Bot) -> MiraiLogger)
MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
}
/** /**
* 网络层日志构造器 * 网络层日志构造器
@ -365,94 +340,22 @@ public open class BotConfiguration { // open for Java
* *
* @see MiraiLogger * @see MiraiLogger
*/ */
public var networkLoggerSupplier: ((Bot) -> MiraiLogger) = { public var networkLoggerSupplier: ((Bot) -> MiraiLogger)
MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
}
/**
* 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
* 默认目录路径为 "$workingDir/logs/".
* @see DirectoryLogger
* @see redirectNetworkLogToDirectory
*/
@JvmOverloads
@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
@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
@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
@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) }
}
/** /**
* 不显示网络日志. 不推荐. * 不显示网络日志. 不推荐.
* @see networkLoggerSupplier 更多日志处理方式 * @see networkLoggerSupplier 更多日志处理方式
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun noNetworkLog() { public fun noNetworkLog()
networkLoggerSupplier = { _ -> SilentLogger }
}
/** /**
* 不显示 [Bot] 日志. 不推荐. * 不显示 [Bot] 日志. 不推荐.
* @see botLoggerSupplier 更多日志处理方式 * @see botLoggerSupplier 更多日志处理方式
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun noBotLog() { public fun noBotLog()
botLoggerSupplier = { _ -> SilentLogger }
}
/** /**
* 是否显示过于冗长的事件日志 * 是否显示过于冗长的事件日志
@ -461,34 +364,17 @@ public open class BotConfiguration { // open for Java
* *
* @since 2.8 * @since 2.8
*/ */
public var isShowingVerboseEventLog: Boolean = false public var isShowingVerboseEventLog: Boolean
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Cache // Cache
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
/**
* 缓存数据目录, 相对于 [workingDir].
*
* 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
* 默认启用的缓存可以加快登录过程.
*
* 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
* - 联系人列表
* - 登录服务器列表
* - 资源服务秘钥
*
* 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
*
* @since 2.4
*/
public var cacheDir: File = File("cache")
/** /**
* 联系人信息缓存配置. 将会保存在 [cacheDir] `contacts` 目录 * 联系人信息缓存配置. 将会保存在 [cacheDir] `contacts` 目录
* @since 2.4 * @since 2.4
*/ */
public var contactListCache: ContactListCache = ContactListCache() public var contactListCache: ContactListCache
/** /**
* 联系人信息缓存配置 * 联系人信息缓存配置
@ -501,26 +387,22 @@ public open class BotConfiguration { // open for Java
/** /**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次. * 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/ */
public var saveIntervalMillis: Long = 60_000 public var saveIntervalMillis: Long
/** /**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次. * 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/ // was @ExperimentalTime before 2.9 */ // was @ExperimentalTime before 2.9
public inline var saveInterval: Duration public var saveInterval: Duration
@JvmSynthetic inline get() = saveIntervalMillis.milliseconds
@JvmSynthetic inline set(v) {
saveIntervalMillis = v.inWholeMilliseconds
}
/** /**
* 开启好友列表缓存. * 开启好友列表缓存.
*/ */
public var friendListCacheEnabled: Boolean = false public var friendListCacheEnabled: Boolean
/** /**
* 开启群成员列表缓存. * 开启群成员列表缓存.
*/ */
public var groupMemberListCacheEnabled: Boolean = false public var groupMemberListCacheEnabled: Boolean
} }
/** /**
@ -534,29 +416,21 @@ public open class BotConfiguration { // open for Java
* @since 2.4 * @since 2.4
*/ */
@JvmSynthetic @JvmSynthetic
public inline fun contactListCache(action: ContactListCache.() -> Unit) { public inline fun contactListCache(action: ContactListCache.() -> Unit)
action.invoke(this.contactListCache)
}
/** /**
* 禁用好友列表和群成员列表的缓存. * 禁用好友列表和群成员列表的缓存.
* @since 2.4 * @since 2.4
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun disableContactCache() { public fun disableContactCache()
contactListCache.friendListCacheEnabled = false
contactListCache.groupMemberListCacheEnabled = false
}
/** /**
* 启用好友列表和群成员列表的缓存. * 启用好友列表和群成员列表的缓存.
* @since 2.4 * @since 2.4
*/ */
@ConfigurationDsl @ConfigurationDsl
public fun enableContactCache() { public fun enableContactCache()
contactListCache.friendListCacheEnabled = true
contactListCache.groupMemberListCacheEnabled = true
}
/** /**
* 登录缓存. * 登录缓存.
@ -570,38 +444,14 @@ public open class BotConfiguration { // open for Java
* *
* @since 2.6 * @since 2.6
*/ */
public var loginCacheEnabled: Boolean = true public var loginCacheEnabled: Boolean
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Misc // Misc
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
public fun copy(): BotConfiguration { public fun copy(): BotConfiguration
return BotConfiguration().also { new ->
// To structural order
new.workingDir = workingDir
@Suppress("DEPRECATION_ERROR")
new.parentCoroutineContext = parentCoroutineContext
new.heartbeatPeriodMillis = heartbeatPeriodMillis
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
new.heartbeatStrategy = heartbeatStrategy
new.reconnectionRetryTimes = reconnectionRetryTimes
new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
new.loginSolver = loginSolver
new.protocol = protocol
new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
new.accountSecrets = accountSecrets
new.deviceInfo = deviceInfo
new.botLoggerSupplier = botLoggerSupplier
new.networkLoggerSupplier = networkLoggerSupplier
new.cacheDir = cacheDir
new.contactListCache = contactListCache
new.convertLineSeparator = convertLineSeparator
new.isShowingVerboseEventLog = isShowingVerboseEventLog
}
}
/** /**
* 是否处理接受到的特殊换行符, 默认为 `true` * 是否处理接受到的特殊换行符, 默认为 `true`
@ -612,28 +462,17 @@ public open class BotConfiguration { // open for Java
* @since 2.4 * @since 2.4
*/ */
@get:JvmName("isConvertLineSeparator") @get:JvmName("isConvertLineSeparator")
public var convertLineSeparator: Boolean = true public var convertLineSeparator: Boolean
/** 标注一个配置 DSL 函数 */ /** 标注一个配置 DSL 函数 */
@Target(AnnotationTarget.FUNCTION) @Target(AnnotationTarget.FUNCTION)
@DslMarker @DslMarker
public annotation class ConfigurationDsl public annotation class ConfigurationDsl()
public companion object { public companion object {
/** 默认的配置实例. 可以进行修改 */ /** 默认的配置实例. 可以进行修改 */
@JvmStatic @JvmStatic
public val Default: BotConfiguration = BotConfiguration() public val Default: BotConfiguration
internal val json: Json = kotlin.runCatching {
Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = true
}
}.getOrElse {
@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatibility for older versions
Json {}
}
} }
} }

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
@ -12,6 +12,7 @@ package net.mamoe.mirai.utils
import kotlinx.io.core.toByteArray import kotlinx.io.core.toByteArray
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.Transient import kotlinx.serialization.Transient
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -21,98 +22,85 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans import net.mamoe.mirai.utils.DeviceInfoManager.Version.Companion.trans
import java.io.File import kotlin.jvm.JvmInline
import kotlin.jvm.JvmStatic
import kotlin.random.Random import kotlin.random.Random
@Serializable public expect class DeviceInfo(
public class DeviceInfo( display: ByteArray,
public val display: ByteArray, product: ByteArray,
public val product: ByteArray, device: ByteArray,
public val device: ByteArray, board: ByteArray,
public val board: ByteArray, brand: ByteArray,
public val brand: ByteArray, model: ByteArray,
public val model: ByteArray, bootloader: ByteArray,
public val bootloader: ByteArray, fingerprint: ByteArray,
public val fingerprint: ByteArray, bootId: ByteArray,
public val bootId: ByteArray, procVersion: ByteArray,
public val procVersion: ByteArray, baseBand: ByteArray,
public val baseBand: ByteArray, version: Version,
public val version: Version, simInfo: ByteArray,
public val simInfo: ByteArray, osType: ByteArray,
public val osType: ByteArray, macAddress: ByteArray,
public val macAddress: ByteArray, wifiBSSID: ByteArray,
public val wifiBSSID: ByteArray, wifiSSID: ByteArray,
public val wifiSSID: ByteArray, imsiMd5: ByteArray,
public val imsiMd5: ByteArray, imei: String,
public val imei: String, apn: ByteArray
public val apn: ByteArray
) { ) {
public val androidId: ByteArray get() = display
public val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
init { public val display: ByteArray
require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." } public val product: ByteArray
} public val device: ByteArray
public val board: ByteArray
public val brand: ByteArray
public val model: ByteArray
public val bootloader: ByteArray
public val fingerprint: ByteArray
public val bootId: ByteArray
public val procVersion: ByteArray
public val baseBand: ByteArray
public val version: Version
public val simInfo: ByteArray
public val osType: ByteArray
public val macAddress: ByteArray
public val wifiBSSID: ByteArray
public val wifiSSID: ByteArray
public val imsiMd5: ByteArray
public val imei: String
public val apn: ByteArray
public val androidId: ByteArray
public val ipAddress: ByteArray
@Transient @Transient
@MiraiInternalApi @MiraiInternalApi
public val guid: ByteArray = generateGuid(androidId, macAddress) public val guid: ByteArray
@Serializable
public class Version( public class Version(
public val incremental: ByteArray = "5891938".toByteArray(), incremental: ByteArray = "5891938".toByteArray(),
public val release: ByteArray = "10".toByteArray(), release: ByteArray = "10".toByteArray(),
public val codename: ByteArray = "REL".toByteArray(), codename: ByteArray = "REL".toByteArray(),
public val sdk: Int = 29 sdk: Int = 29
) { ) {
/** public val incremental: ByteArray
* @since 2.9 public val release: ByteArray
*/ public val codename: ByteArray
override fun equals(other: Any?): Boolean { public val sdk: Int
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Version
if (!incremental.contentEquals(other.incremental)) return false
if (!release.contentEquals(other.release)) return false
if (!codename.contentEquals(other.codename)) return false
if (sdk != other.sdk) return false
return true
}
/** /**
* @since 2.9 * @since 2.9
*/ */
override fun hashCode(): Int { override fun equals(other: Any?): Boolean
var result = incremental.contentHashCode()
result = 31 * result + release.contentHashCode() /**
result = 31 * result + codename.contentHashCode() * @since 2.9
result = 31 * result + sdk */
return result override fun hashCode(): Int
}
} }
public companion object { public companion object {
internal val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo") internal val logger: MiraiLogger
/**
* 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
*/
@JvmOverloads
@JvmStatic
@JvmName("from")
public fun File.loadAsDeviceInfo(
json: Json = DeviceInfoManager.format
): DeviceInfo {
if (!this.exists() || this.length() == 0L) {
return random().also {
this.writeText(DeviceInfoManager.serialize(it, json))
}
}
return DeviceInfoManager.deserialize(this.readText(), json)
}
/** /**
* 生成随机 [DeviceInfo] * 生成随机 [DeviceInfo]
@ -120,7 +108,7 @@ public class DeviceInfo(
* @since 2.0 * @since 2.0
*/ */
@JvmStatic @JvmStatic
public fun random(): DeviceInfo = random(Random.Default) public fun random(): DeviceInfo
/** /**
* 使用特定随机数生成器生成 [DeviceInfo] * 使用特定随机数生成器生成 [DeviceInfo]
@ -128,61 +116,72 @@ public class DeviceInfo(
* @since 2.9 * @since 2.9
*/ */
@JvmStatic @JvmStatic
public fun random(random: Random): DeviceInfo { public fun random(random: Random): DeviceInfo
return DeviceInfo(
display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
product = "mirai".toByteArray(),
device = "mirai".toByteArray(),
board = "mirai".toByteArray(),
brand = "mamoe".toByteArray(),
model = "mirai".toByteArray(),
bootloader = "unknown".toByteArray(),
fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${
getRandomIntString(7, random)
}:user/release-keys".toByteArray(),
bootId = generateUUID(getRandomByteArray(16, random).md5()).toByteArray(),
procVersion = "Linux version 3.0.31-${
getRandomString(8, random)
} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
baseBand = byteArrayOf(),
version = Version(),
simInfo = "T-Mobile".toByteArray(),
osType = "android".toByteArray(),
macAddress = "02:00:00:00:00:00".toByteArray(),
wifiBSSID = "02:00:00:00:00:00".toByteArray(),
wifiSSID = "<unknown ssid>".toByteArray(),
imsiMd5 = getRandomByteArray(16, random).md5(),
imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
apn = "wifi".toByteArray()
)
}
/**
* 计算 imei 校验位
*/
private fun luhn(imei: String): Int {
var odd = false
val zero = '0'
val sum = imei.sumOf { char ->
odd = !odd
if (odd) {
char.code - zero.code
} else {
val s = (char.code - zero.code) * 2
s % 10 + s / 10
}
}
return (10 - sum % 10) % 10
}
} }
/** /**
* @since 2.9 * @since 2.9
*/ */
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean
if (this === other) return true
if (javaClass != other?.javaClass) return false /**
* @since 2.9
*/
override fun hashCode(): Int
}
internal object DeviceInfoCommonImpl {
fun randomDeviceInfo(random: Random) = DeviceInfo(
display = "MIRAI.${getRandomString(6, '0'..'9', random)}.001".toByteArray(),
product = "mirai".toByteArray(),
device = "mirai".toByteArray(),
board = "mirai".toByteArray(),
brand = "mamoe".toByteArray(),
model = "mirai".toByteArray(),
bootloader = "unknown".toByteArray(),
fingerprint = "mamoe/mirai/mirai:10/MIRAI.200122.001/${
getRandomIntString(7, random)
}:user/release-keys".toByteArray(),
bootId = generateUUID(getRandomByteArray(16, random).md5()).toByteArray(),
procVersion = "Linux version 3.0.31-${
getRandomString(8, random)
} (android-build@xxx.xxx.xxx.xxx.com)".toByteArray(),
baseBand = byteArrayOf(),
version = DeviceInfo.Version(),
simInfo = "T-Mobile".toByteArray(),
osType = "android".toByteArray(),
macAddress = "02:00:00:00:00:00".toByteArray(),
wifiBSSID = "02:00:00:00:00:00".toByteArray(),
wifiSSID = "<unknown ssid>".toByteArray(),
imsiMd5 = getRandomByteArray(16, random).md5(),
imei = "86${getRandomIntString(12, random)}".let { it + luhn(it) },
apn = "wifi".toByteArray()
)
/**
* 计算 imei 校验位
*/
private fun luhn(imei: String): Int {
var odd = false
val zero = '0'
val sum = imei.sumOf { char ->
odd = !odd
if (odd) {
char.code - zero.code
} else {
val s = (char.code - zero.code) * 2
s % 10 + s / 10
}
}
return (10 - sum % 10) % 10
}
@Suppress("DuplicatedCode")
fun equalsImpl(deviceInfo: DeviceInfo, other: Any?): Boolean = deviceInfo.run {
if (deviceInfo === other) return true
if (other !is DeviceInfo) return false
other as DeviceInfo other as DeviceInfo
@ -211,10 +210,8 @@ public class DeviceInfo(
return true return true
} }
/** @Suppress("DuplicatedCode")
* @since 2.9 fun hashCodeImpl(deviceInfo: DeviceInfo): Int = deviceInfo.run {
*/
override fun hashCode(): Int {
var result = display.contentHashCode() var result = display.contentHashCode()
result = 31 * result + product.contentHashCode() result = 31 * result + product.contentHashCode()
result = 31 * result + device.contentHashCode() result = 31 * result + device.contentHashCode()
@ -293,6 +290,9 @@ internal object DeviceInfoManager {
val data: T val data: T
) )
@Serializer(forClass = DeviceInfo.Version::class)
private object DeviceInfoVersionSerializer
@Serializable @Serializable
class V1( class V1(
val display: ByteArray, val display: ByteArray,
@ -306,7 +306,7 @@ internal object DeviceInfoManager {
val bootId: ByteArray, val bootId: ByteArray,
val procVersion: ByteArray, val procVersion: ByteArray,
val baseBand: ByteArray, val baseBand: ByteArray,
val version: DeviceInfo.Version, val version: @Serializable(DeviceInfoVersionSerializer::class) DeviceInfo.Version,
val simInfo: ByteArray, val simInfo: ByteArray,
val osType: ByteArray, val osType: ByteArray,
val macAddress: ByteArray, val macAddress: ByteArray,
@ -472,7 +472,7 @@ internal object DeviceInfoManager {
* Defaults "%4;7t>;28<fc.5*6".toByteArray() * Defaults "%4;7t>;28<fc.5*6".toByteArray()
*/ */
@Suppress("RemoveRedundantQualifierName") // bug @Suppress("RemoveRedundantQualifierName") // bug
private fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray = internal fun generateGuid(androidId: ByteArray, macAddress: ByteArray): ByteArray =
(androidId + macAddress).md5() (androidId + macAddress).md5()

View File

@ -11,28 +11,24 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.io.core.Input
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge 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
import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.contact.Contact.Companion.sendImage
import net.mamoe.mirai.contact.Contact.Companion.uploadImage 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.internal.utils.*
import net.mamoe.mirai.message.MessageReceipt 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.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
import java.io.*
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/** /**
@ -124,7 +120,7 @@ import kotlin.contracts.contract
* *
* @see FileCacheStrategy * @see FileCacheStrategy
*/ */
public interface ExternalResource : Closeable { public expect interface ExternalResource : Closeable {
/** /**
* 是否在 _使用一次_ 后自动 [close]. * 是否在 _使用一次_ 后自动 [close].
@ -136,8 +132,7 @@ public interface ExternalResource : Closeable {
* @since 2.8 * @since 2.8
*/ */
@MiraiExperimentalApi @MiraiExperimentalApi
public val isAutoClose: Boolean public open val isAutoClose: Boolean
get() = false
/** /**
* 文件内容 MD5. 16 bytes * 文件内容 MD5. 16 bytes
@ -148,11 +143,7 @@ public interface ExternalResource : Closeable {
* 文件内容 SHA1. 16 bytes * 文件内容 SHA1. 16 bytes
* @since 2.5 * @since 2.5
*/ */
public val sha1: ByteArray public open val sha1: ByteArray
get() =
throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}")
// 如果你要实现 [ExternalResource], 你也应该实现 [sha1].
// 这里默认抛出 [UnsupportedOperationException] 是为了 (姑且) 兼容 2.5 以前的版本的实现.
/** /**
@ -178,17 +169,17 @@ public interface ExternalResource : Closeable {
public val closed: Deferred<Unit> public val closed: Deferred<Unit>
/** /**
* 打开 [InputStream]. 在返回的 [InputStream] [关闭][InputStream.close] 前无法再次打开流. * 打开 [Input]. 在返回的 [Input] [关闭][Input.close] 前无法再次打开流.
* *
* 关闭此流不会关闭 [ExternalResource]. * 关闭此流不会关闭 [ExternalResource].
* @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出 * @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
*
* @since SINCE_NATIVE_TARGET
*/ */
public fun inputStream(): InputStream public fun input(): Input
@MiraiInternalApi @MiraiInternalApi
public fun calculateResourceId(): String { public open fun calculateResourceId(): String
return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
}
/** /**
* [ExternalResource] 的数据来源, 可能有以下的返回 * [ExternalResource] 的数据来源, 可能有以下的返回
@ -209,25 +200,14 @@ public interface ExternalResource : Closeable {
* *
* @since 2.8.0 * @since 2.8.0
*/ */
public val origin: Any? get() = null public open val origin: Any?
/** /**
* 创建一个在 _使用一次_ 后就会自动 [close] [ExternalResource]. * 创建一个在 _使用一次_ 后就会自动 [close] [ExternalResource].
* *
* @since 2.8.0 * @since 2.8.0
*/ */
public fun toAutoCloseable(): ExternalResource { public open 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 { public companion object {
@ -236,48 +216,12 @@ public interface ExternalResource : Closeable {
* *
* @see ExternalResource.formatName * @see ExternalResource.formatName
*/ */
public const val DEFAULT_FORMAT_NAME: String = "mirai" public val DEFAULT_FORMAT_NAME: String
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// region toExternalResource // 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] 关闭. * 创建 [ExternalResource]. 注意, 返回的 [ExternalResource] 需要在使用完毕后调用 [ExternalResource.close] 关闭.
* *
@ -286,67 +230,10 @@ public interface ExternalResource : Closeable {
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@JvmName("create") @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 // 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 // region sendAsImageTo
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -364,43 +251,7 @@ public interface ExternalResource : Closeable {
@JvmBlockingBridge @JvmBlockingBridge
@JvmStatic @JvmStatic
@JvmName("sendAsImage") @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 // endregion
@ -419,435 +270,9 @@ public interface ExternalResource : Closeable {
*/ */
@JvmStatic @JvmStatic
@JvmBlockingBridge @JvmBlockingBridge
public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this) public suspend fun ExternalResource.uploadAsImage(contact: Contact): Image
/**
* 读取 [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 // 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.12")
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.12")
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)
.let { net.mamoe.mirai.message.data.Voice.fromAudio(it) }
else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
}
// endregion
}
}
/**
* 一个实现了基本方法的外部资源
*
* ## 实现
*
* [AbstractExternalResource] 实现了大部分必要的方法,
* 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现
*
* 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的
*
* Example:
* ```
* class MyCustomExternalResource: AbstractExternalResource() {
* override fun inputStream0(): InputStream = FileInputStream("/test.txt")
* override val size: Long get() = File("/test.txt").length()
* }
* ```
*
* ## 资源释放
*
* 如同 mirai 内置的 [ExternalResource] 实现一样,
* [AbstractExternalResource] 也会被注册进入资源泄露监视器
* (即意味着 [AbstractExternalResource] 也要求手动关闭)
*
* 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法,
* 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放
*
* 对于 [ResourceCleanCallback], 有以下要求
*
* - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用)
*
* Example:
* ```
* class MyRes(
* cleanup: ResourceCleanCallback,
* val delegate: Closable,
* ): AbstractExternalResource(cleanup) {
* }
*
* // 错误, 该写法会导致 Resource 永远也不会被自动释放
* lateinit var myRes: MyRes
* val cleanup = ResourceCleanCallback {
* myRes.delegate.close()
* }
* myRes = MyRes(cleanup, fetchDelegate())
*
* // 正确
* val delegate: Closable
* val cleanup = ResourceCleanCallback {
* delegate.close()
* }
* val myRes = MyRes(cleanup, delegate)
* ```
*
* @since 2.9
*
* @see ExternalResource
* @see AbstractExternalResource.setResourceCleanCallback
* @see AbstractExternalResource.registerToLeakObserver
*/
@Suppress("MemberVisibilityCanBePrivate")
public abstract class AbstractExternalResource
@JvmOverloads
public constructor(
displayName: String? = null,
cleanup: ResourceCleanCallback? = null,
) : ExternalResource {
public constructor(
cleanup: ResourceCleanCallback? = null,
) : this(null, cleanup)
public fun interface ResourceCleanCallback {
@Throws(IOException::class)
public fun cleanup()
}
override val md5: ByteArray by lazy { inputStream().md5() }
override val sha1: ByteArray by lazy { inputStream().sha1() }
override val formatName: String by lazy {
inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME
}
private val leakObserverRegistered = atomic(false)
/**
* 注册 [ExternalResource] 资源泄露监视器
*
* 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器
*
* 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露
*
* 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露
*
* 正确示例:
* ```
* // Kotlin
* public class MyResource: AbstractExternalResource() {
* init {
* val res: SomeResource
* // 一些资源初始化
* registerToLeakObserver()
* setResourceCleanCallback(Releaser(res))
* }
*
* private class Releaser(
* private val res: SomeResource,
* ) : AbstractExternalResource.ResourceCleanCallback {
* override fun cleanup() = res.close()
* }
* }
*
* // Java
* public class MyResource extends AbstractExternalResource {
* public MyResource() throws IOException {
* SomeResource res;
* // 一些资源初始化
* registerToLeakObserver();
* setResourceCleanCallback(new Releaser(res));
* }
*
* private static class Releaser implements ResourceCleanCallback {
* private final SomeResource res;
* Releaser(SomeResource res) { this.res = res; }
*
* public void cleanup() throws IOException { res.close(); }
* }
* }
* ```
*
* @see setResourceCleanCallback
*/
protected fun registerToLeakObserver() {
// 用户自定义 AbstractExternalResource 也许会在 <init> 的时候失败
// 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver
if (leakObserverRegistered.compareAndSet(expect = false, update = true)) {
ExternalResourceLeakObserver.register(this, holder)
}
}
/**
* 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器
* **仅在我知道我在干什么的前提下调用此方法**
*
* 不建议取消注册监视器, 这可能带来意外的错误
*
* @see registerToLeakObserver
*/
protected fun dontRegisterLeakObserver() {
leakObserverRegistered.value = true
}
final override fun inputStream(): InputStream {
registerToLeakObserver()
return inputStream0()
}
protected abstract fun inputStream0(): InputStream
/**
* 修改 `this` 的资源释放回调
* **仅在我知道我在干什么的前提下调用此方法**
*
* ```
* class MyRes {
* // region kotlin
*
* private inner class Releaser : ResourceCleanCallback
*
* private class NotInnerReleaser : ResourceCleanCallback
*
* init {
* // 错误, 内部类, Releaser 存在对 MyRes 的引用
* setResourceCleanCallback(Releaser())
* // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器
* setResourceCleanCallback(object : ResourceCleanCallback {})
* // 正确, 无 inner 修饰, 等同于 java 的 private static class
* setResourceCleanCallback(NotInnerReleaser(directResource))
* }
*
* // endregion kotlin
*
* // region java
*
* private class Releaser implements ResourceCleanCallback {}
* private static class StaticReleaser implements ResourceCleanCallback {}
*
* MyRes() {
* // 错误, 内部类, 存在对 MyRes 的引用
* setResourceCleanCallback(new Releaser());
* // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac
* setResourceCleanCallback(new ResourceCleanCallback() {});
* // 正确
* setResourceCleanCallback(new StaticReleaser(directResource));
* }
*
* // endregion java
* }
* ```
*
* @see registerToLeakObserver
*/
protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) {
holder.cleanup = cleanup
}
private class UsrCustomResHolder(
@JvmField var cleanup: ResourceCleanCallback?,
private val resourceName: String,
) : ExternalResourceHolder() {
override val closed: Deferred<Unit> = CompletableDeferred()
override fun closeImpl() {
cleanup?.cleanup()
}
// display on logger of ExternalResourceLeakObserver
override fun toString(): String = resourceName
}
private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString {
append("ExternalResourceHolder<")
append(this@AbstractExternalResource.javaClass.name)
append('@')
append(System.identityHashCode(this@AbstractExternalResource))
append('>')
})
final override val closed: Deferred<Unit> get() = holder.closed.also { registerToLeakObserver() }
@Throws(IOException::class)
final override fun close() {
holder.close()
} }
} }

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:Suppress("unused", "MemberVisibilityCanBePrivate") @file:Suppress("unused", "MemberVisibilityCanBePrivate")
@ -12,6 +12,7 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.io.errors.IOException
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.IMirai import net.mamoe.mirai.IMirai
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
@ -20,8 +21,8 @@ import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
import net.mamoe.mirai.utils.FileCacheStrategy.MemoryCache import net.mamoe.mirai.utils.FileCacheStrategy.MemoryCache
import net.mamoe.mirai.utils.FileCacheStrategy.TempCache import net.mamoe.mirai.utils.FileCacheStrategy.TempCache
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import kotlin.jvm.JvmOverloads
/** /**
* 资源缓存策略. * 资源缓存策略.

View File

@ -11,9 +11,8 @@ package net.mamoe.mirai.utils
import net.mamoe.mirai.Bot import net.mamoe.mirai.Bot
import net.mamoe.mirai.network.LoginFailedException import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
import net.mamoe.mirai.utils.LoginSolver.Companion.Default import net.mamoe.mirai.utils.LoginSolver.Companion.Default
import java.io.File import kotlin.jvm.JvmField
/** /**
* 验证码, 设备锁解决器 * 验证码, 设备锁解决器
@ -81,10 +80,3 @@ public expect abstract class LoginSolver() {
} }
} }
internal fun getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
return {
@Suppress("DEPRECATION_ERROR")
file().loadAsDeviceInfo(BotConfiguration.json)
}
}

View File

@ -7,13 +7,15 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:Suppress("unused")
@file:JvmMultifileClass @file:JvmMultifileClass
@file:JvmName("Utils") @file:JvmName("Utils")
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import java.util.* import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@ -46,7 +48,7 @@ public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitc
* *
* @see MiraiLoggerPlatformBase 平台通用基础实现. Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数. * @see MiraiLoggerPlatformBase 平台通用基础实现. Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数.
*/ */
public interface MiraiLogger { public expect interface MiraiLogger {
/** /**
* 可以 service 实现的方式覆盖. * 可以 service 实现的方式覆盖.
@ -60,32 +62,16 @@ public interface MiraiLogger {
* @param requester 请求创建 [MiraiLogger] 的对象的 class * @param requester 请求创建 [MiraiLogger] 的对象的 class
* @param identity 对象标记 (备注) * @param identity 对象标记 (备注)
*/ */
public fun create(requester: KClass<*>, identity: String? = null): MiraiLogger = public open 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] 实例. * 创建 [MiraiLogger] 实例.
* *
* @param requester 请求创建 [MiraiLogger] 的对象 * @param requester 请求创建 [MiraiLogger] 的对象
*/ */
public fun create(requester: KClass<*>): MiraiLogger = create(requester, null) public open fun create(requester: KClass<*>): MiraiLogger
/** public companion object INSTANCE : Factory
* 创建 [MiraiLogger] 实例.
*
* @param requester 请求创建 [MiraiLogger] 的对象
*/
public fun create(requester: Class<*>): MiraiLogger = create(requester, null)
public companion object INSTANCE : Factory by loadService(Factory::class, { DefaultFactory() })
} }
public companion object { public companion object {
@ -96,7 +82,7 @@ public interface MiraiLogger {
@MiraiExperimentalApi @MiraiExperimentalApi
@Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 @Deprecated("Deprecated.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") } public val TopLevel: MiraiLogger
/** /**
* 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供. * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
@ -108,9 +94,8 @@ public interface MiraiLogger {
) // deprecated since 2.7 ) // deprecated since 2.7
@JvmStatic @JvmStatic
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses. @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
public fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) { public fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger)
DefaultFactoryOverrides.override { _, identity -> creator(identity) }
}
/** /**
* 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create]. * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
@ -125,7 +110,7 @@ public interface MiraiLogger {
) // deprecated since 2.7 ) // deprecated since 2.7
@JvmStatic @JvmStatic
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity) public fun create(identity: String?): MiraiLogger
} }
/** /**
@ -154,7 +139,7 @@ public interface MiraiLogger {
* *
* @since 2.7 * @since 2.7
*/ */
public val isVerboseEnabled: Boolean get() = isEnabled public open val isVerboseEnabled: Boolean
/** /**
* DEBUG 级别的日志启用时返回 `true` * DEBUG 级别的日志启用时返回 `true`
@ -165,7 +150,7 @@ public interface MiraiLogger {
* *
* @since 2.7 * @since 2.7
*/ */
public val isDebugEnabled: Boolean get() = isEnabled public open val isDebugEnabled: Boolean
/** /**
* INFO 级别的日志启用时返回 `true` * INFO 级别的日志启用时返回 `true`
@ -176,7 +161,7 @@ public interface MiraiLogger {
* *
* @since 2.7 * @since 2.7
*/ */
public val isInfoEnabled: Boolean get() = isEnabled public open val isInfoEnabled: Boolean
/** /**
* WARNING 级别的日志启用时返回 `true` * WARNING 级别的日志启用时返回 `true`
@ -187,7 +172,7 @@ public interface MiraiLogger {
* *
* @since 2.7 * @since 2.7
*/ */
public val isWarningEnabled: Boolean get() = isEnabled public open val isWarningEnabled: Boolean
/** /**
* ERROR 级别的日志启用时返回 `true` * ERROR 级别的日志启用时返回 `true`
@ -198,7 +183,7 @@ public interface MiraiLogger {
* *
* @since 2.7 * @since 2.7
*/ */
public val isErrorEnabled: Boolean get() = isEnabled public open val isErrorEnabled: Boolean
/** /**
* 随从. this 中调用所有方法后都应继续往 [follower] 传递调用. * 随从. this 中调用所有方法后都应继续往 [follower] 传递调用.
@ -213,9 +198,7 @@ public interface MiraiLogger {
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
@Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public var follower: MiraiLogger? public open var follower: MiraiLogger?
get() = null
set(value) {}
/** /**
* 记录一个 `verbose` 级别的日志. * 记录一个 `verbose` 级别的日志.
@ -223,7 +206,7 @@ public interface MiraiLogger {
*/ */
public fun verbose(message: String?) public fun verbose(message: String?)
public fun verbose(e: Throwable?): Unit = verbose(null, e) public open fun verbose(e: Throwable?): Unit
public fun verbose(message: String?, e: Throwable?) public fun verbose(message: String?, e: Throwable?)
/** /**
@ -231,7 +214,7 @@ public interface MiraiLogger {
*/ */
public fun debug(message: String?) public fun debug(message: String?)
public fun debug(e: Throwable?): Unit = debug(null, e) public open fun debug(e: Throwable?): Unit
public fun debug(message: String?, e: Throwable?) public fun debug(message: String?, e: Throwable?)
@ -240,7 +223,7 @@ public interface MiraiLogger {
*/ */
public fun info(message: String?) public fun info(message: String?)
public fun info(e: Throwable?): Unit = info(null, e) public open fun info(e: Throwable?): Unit
public fun info(message: String?, e: Throwable?) public fun info(message: String?, e: Throwable?)
@ -249,7 +232,7 @@ public interface MiraiLogger {
*/ */
public fun warning(message: String?) public fun warning(message: String?)
public fun warning(e: Throwable?): Unit = warning(null, e) public open fun warning(e: Throwable?): Unit
public fun warning(message: String?, e: Throwable?) public fun warning(message: String?, e: Throwable?)
@ -258,12 +241,11 @@ public interface MiraiLogger {
*/ */
public fun error(message: String?) public fun error(message: String?)
public fun error(e: Throwable?): Unit = error(null, e) public open fun error(e: Throwable?): Unit
public fun error(message: String?, e: Throwable?) public fun error(message: String?, e: Throwable?)
/** 根据优先级调用对应函数 */ /** 根据优先级调用对应函数 */
public fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null): Unit = public open fun call(priority: SimpleLogger.LogPriority, message: String? = null, e: Throwable? = null): Unit
priority.correspondingFunction(this, message, e)
/** /**
* 添加一个 [follower], 返回 [follower] * 添加一个 [follower], 返回 [follower]
@ -280,7 +262,7 @@ public interface MiraiLogger {
@Suppress("DeprecatedCallableAddReplaceWith") @Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7 @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public operator fun <T : MiraiLogger> plus(follower: T): T = follower public open operator fun <T : MiraiLogger> plus(follower: T): T
} }
@ -559,27 +541,3 @@ public abstract class MiraiLoggerPlatformBase : MiraiLogger {
return follower return follower
} }
} }
internal object DefaultFactoryOverrides {
var override: ((requester: Class<*>, identity: String?) -> MiraiLogger)? =
null // 支持 LoggerAdapters 以及兼容旧版本
@JvmStatic
fun override(lambda: (requester: Class<*>, identity: String?) -> MiraiLogger) {
override = lambda
}
@JvmStatic
fun clearOverride() {
override = null
}
}
internal class DefaultFactory : MiraiLogger.Factory {
override fun create(requester: Class<*>, identity: String?): MiraiLogger {
val override = DefaultFactoryOverrides.override
return if (override != null) override(requester, identity) else PlatformLogger(
identity ?: requester.kotlin.simpleName ?: requester.simpleName
)
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -12,6 +12,9 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/** /**
* 图片文件过大 * 图片文件过大
*/ // 不要删除多平台结构, 这是 kotlin 的 bug */ // 不要删除多平台结构, 这是 kotlin 的 bug

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -12,6 +12,7 @@ package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import net.mamoe.mirai.contact.file.AbsoluteFile import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.utils.ProgressionCallback.Companion.asProgressionCallback import net.mamoe.mirai.utils.ProgressionCallback.Companion.asProgressionCallback
import kotlin.jvm.JvmStatic
/** /**

View File

@ -8,25 +8,21 @@
*/ */
@file:Suppress("unused", "DEPRECATION") @file:Suppress("unused", "DEPRECATION")
@file:JvmBlockingBridge
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.toList
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.FileSupported import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.Group import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
import java.io.File import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/** /**
* 表示一个远程文件或目录. * 表示一个远程文件或目录.
@ -97,10 +93,13 @@ import java.io.File
* @see FileSupported * @see FileSupported
* @since 2.5 * @since 2.5
*/ */
@Deprecated("Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files", level = DeprecationLevel.WARNING) // deprecated since 2.8.0-RC @Deprecated(
"Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8") @DeprecatedSinceMirai(warningSince = "2.8")
@NotStableForInheritance @NotStableForInheritance
public interface RemoteFile { public expect interface RemoteFile {
/** /**
* 文件名或目录名. * 文件名或目录名.
*/ */
@ -136,7 +135,7 @@ public interface RemoteFile {
/** /**
* [RemoteFile] 表示一个目录时返回 `true`. * [RemoteFile] 表示一个目录时返回 `true`.
*/ */
public suspend fun isDirectory(): Boolean = !isFile() public open suspend fun isDirectory(): Boolean
/** /**
* 获取文件长度. [RemoteFile] 表示一个目录时行为不确定. * 获取文件长度. [RemoteFile] 表示一个目录时行为不确定.
@ -144,46 +143,64 @@ public interface RemoteFile {
public suspend fun length(): Long public suspend fun length(): Long
public class FileInfo @MiraiInternalApi constructor( public class FileInfo @MiraiInternalApi constructor(
name: String,
id: String,
path: String,
length: Long,
downloadTimes: Int,
uploaderId: Long,
uploadTime: Long,
lastModifyTime: Long,
sha1: ByteArray,
md5: ByteArray,
) {
/** /**
* 文件或目录名. * 文件或目录名.
*/ */
public val name: String, public val name: String
/** /**
* 唯一识别标识. * 唯一识别标识.
*/ */
public val id: String, public val id: String
/** /**
* 标准绝对路径. * 标准绝对路径.
*/ */
public val path: String, public val path: String
/** /**
* 文件长度 (大小) bytes, 目录的 [length] 0. * 文件长度 (大小) bytes, 目录的 [length] 0.
*/ */
public val length: Long, public val length: Long
/** /**
* 下载次数. 目录没有下载次数, 此属性总是 `0`. * 下载次数. 目录没有下载次数, 此属性总是 `0`.
*/ */
public val downloadTimes: Int, public val downloadTimes: Int
/** /**
* 上传者 ID. 目录没有上传者, 此属性总是 `0`. * 上传者 ID. 目录没有上传者, 此属性总是 `0`.
*/ */
public val uploaderId: Long, public val uploaderId: Long
/** /**
* 上传的时间. 目录没有上传时间, 此属性总是 `0`. * 上传的时间. 目录没有上传时间, 此属性总是 `0`.
*/ */
public val uploadTime: Long, public val uploadTime: Long
/** /**
* 上次修改时间. 时间戳秒. * 上次修改时间. 时间戳秒.
*/ */
public val lastModifyTime: Long, public val lastModifyTime: Long
public val sha1: ByteArray, public val sha1: ByteArray
public val md5: ByteArray, public val md5: ByteArray
) {
/** /**
* 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile]. * 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile].
*/ */
public suspend fun resolveToFile(contact: FileSupported): RemoteFile = public suspend fun resolveToFile(contact: FileSupported): RemoteFile
contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
} }
/** /**
@ -231,7 +248,7 @@ public interface RemoteFile {
* 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null` * 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null`
* @see resolve * @see resolve
*/ */
public suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true) public open suspend fun resolveById(id: String): RemoteFile?
/** /**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`. * 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
@ -304,12 +321,7 @@ public interface RemoteFile {
level = DeprecationLevel.ERROR level = DeprecationLevel.ERROR
) // deprecated since 2.7 ) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
public suspend fun moveTo(path: String): Boolean { public open suspend fun moveTo(path: String): Boolean
// Impl notes:
// if `path` is absolute, this works as intended.
// if not, `resolve(path)` will be a child path from this dir and fails always.
return moveTo(resolve(path))
}
/** /**
* 创建目录. 目录已经存在或无管理员权限时返回 `false`. * 创建目录. 目录已经存在或无管理员权限时返回 `false`.
@ -336,7 +348,7 @@ public interface RemoteFile {
/** /**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList]. * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList].
*/ */
public suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList() public open suspend fun listFilesCollection(): List<RemoteFile>
/** /**
* 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`. * 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
@ -361,24 +373,24 @@ public interface RemoteFile {
/** /**
* 当上传开始时调用 * 当上传开始时调用
*/ */
public fun onBegin(file: RemoteFile, resource: ExternalResource) {} public open fun onBegin(file: RemoteFile, resource: ExternalResource)
/** /**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用. * 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
* *
* 提示: 可通过 [ExternalResource.size] 获取文件总大小. * 提示: 可通过 [ExternalResource.size] 获取文件总大小.
*/ */
public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {} public open fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long)
/** /**
* 当上传成功时调用 * 当上传成功时调用
*/ */
public fun onSuccess(file: RemoteFile, resource: ExternalResource) {} public open fun onSuccess(file: RemoteFile, resource: ExternalResource)
/** /**
* 当上传以异常失败时调用 * 当上传以异常失败时调用
*/ */
public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {} public open fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable)
public companion object { public companion object {
/** /**
@ -407,21 +419,7 @@ public interface RemoteFile {
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作. * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
*/ */
@JvmStatic @JvmStatic
public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback { public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback
return object : ProgressionCallback {
override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
trySend(downloadedSize)
}
override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
if (closeOnFinish) this@asProgressionCallback.close()
}
override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
if (closeOnFinish) this@asProgressionCallback.close(exception)
}
}
}
} }
} }
@ -475,36 +473,7 @@ public interface RemoteFile {
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR "Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1 ) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally @DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null) public open suspend fun upload(resource: ExternalResource): FileMessage
/**
* 上传文件.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(file, callback)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun upload(
file: File,
callback: ProgressionCallback? = null,
): FileMessage = file.toExternalResource().use { upload(it, callback) }
/**
* 上传文件.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use sendFile instead.", ReplaceWith("this.uploadAndSend(file)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun upload(file: File): FileMessage = file.toExternalResource().use { upload(it) }
/** /**
* 上传文件并发送文件消息. * 上传文件并发送文件消息.
@ -519,44 +488,43 @@ public interface RemoteFile {
@MiraiExperimentalApi @MiraiExperimentalApi
public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
/**
* 上传文件并发送文件消息.
* @see uploadAndSend
*/
@MiraiExperimentalApi
public suspend fun uploadAndSend(file: File): MessageReceipt<Contact> =
file.toExternalResource().use { uploadAndSend(it) }
/** /**
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null` * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
*/ */
public suspend fun getDownloadInfo(): DownloadInfo? public suspend fun getDownloadInfo(): DownloadInfo?
public class DownloadInfo @MiraiInternalApi constructor( public class DownloadInfo @MiraiInternalApi constructor(
filename: String,
id: String,
path: String,
url: String,
sha1: ByteArray,
md5: ByteArray,
) {
/** /**
* @see RemoteFile.name * @see RemoteFile.name
*/ */
public val filename: String, public val filename: String
/** /**
* @see RemoteFile.id * @see RemoteFile.id
*/ */
public val id: String, public val id: String
/** /**
* 标准绝对路径 * 标准绝对路径
* @see RemoteFile.path * @see RemoteFile.path
*/ */
public val path: String, public val path: String
/** /**
* HTTP or HTTPS URL * HTTP or HTTPS URL
*/ */
public val url: String, public val url: String
public val sha1: ByteArray, public val sha1: ByteArray
public val md5: ByteArray, public val md5: ByteArray
) { override fun toString(): String
override fun toString(): String {
return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
"md5=${md5.toUHexString("")})"
}
} }
public companion object { public companion object {
@ -564,7 +532,7 @@ public interface RemoteFile {
* 根目录路径 * 根目录路径
* @see RemoteFile.path * @see RemoteFile.path
*/ */
public const val ROOT_PATH: String = "/" public val ROOT_PATH: String
/** /**
* 上传文件并获取文件消息, 但不发送. * 上传文件并获取文件消息, 但不发送.
@ -591,32 +559,7 @@ public interface RemoteFile {
path: String, path: String,
resource: ExternalResource, resource: ExternalResource,
callback: ProgressionCallback? = null, callback: ProgressionCallback? = null,
): FileMessage = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback) ): FileMessage
/**
* 上传文件并获取文件消息, 但不发送.
* ## 已弃用
* 阅读 [uploadFile] 获取更多信息.
*
* @param path 远程路径. 起始字符为 '/'. '/foo/bar.txt'
* @see RemoteFile.upload
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Use sendFile instead.",
ReplaceWith(
"this.sendFile(path, file, callback)",
"net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
),
level = DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun FileSupported.uploadFile(
path: String,
file: File,
callback: ProgressionCallback? = null,
): FileMessage = @Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(file, callback)
/** /**
* 上传文件并发送文件消息到相关 [FileSupported]. * 上传文件并发送文件消息到相关 [FileSupported].
@ -635,28 +578,6 @@ public interface RemoteFile {
path: String, path: String,
resource: ExternalResource, resource: ExternalResource,
callback: ProgressionCallback? = null, callback: ProgressionCallback? = null,
): MessageReceipt<C> = ): MessageReceipt<C>
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
/**
* 上传文件并发送文件消息到相关 [FileSupported].
* @see RemoteFile.uploadAndSend
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public suspend fun <C : FileSupported> C.sendFile(
path: String,
file: File,
callback: ProgressionCallback? = null,
): MessageReceipt<C> =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(file, callback).sendTo(this)
} }
} }

View File

@ -10,7 +10,7 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import net.mamoe.mirai.utils.safeCast import net.mamoe.mirai.utils.safeCast
import org.junit.jupiter.api.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue

View File

@ -11,7 +11,7 @@ package net.mamoe.mirai.message.data
import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit import net.mamoe.mirai.message.data.visitor.MessageVisitorUnit
import net.mamoe.mirai.message.data.visitor.acceptChildren import net.mamoe.mirai.message.data.visitor.acceptChildren
import org.junit.jupiter.api.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs import kotlin.test.assertIs

View File

@ -9,7 +9,7 @@
package net.mamoe.mirai.message.data package net.mamoe.mirai.message.data
import org.junit.jupiter.api.Test import kotlin.test.Test
import kotlin.test.assertIs import kotlin.test.assertIs
internal class MessageChainImplTest { internal class MessageChainImplTest {

View File

@ -13,7 +13,7 @@ import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.message.data.visitor.AbstractMessageVisitor import net.mamoe.mirai.message.data.visitor.AbstractMessageVisitor
import net.mamoe.mirai.message.data.visitor.accept import net.mamoe.mirai.message.data.visitor.accept
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import org.junit.jupiter.api.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
internal class MessageVisitorTest { internal class MessageVisitorTest {

View File

@ -0,0 +1,111 @@
/*
* 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
@file:Suppress("INAPPLICABLE_JVM_NAME")
package net.mamoe.mirai.contact.announcement
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance
import java.util.stream.Stream
/**
* 表示一个群的公告列表 (管理器).
*
* ## 获取群公告
*
* ### 获取 [Announcements] 实例
*
* 只可以通过 [Group.announcements] 获取一个群的公告列表, [Announcements] 实例.
*
* ### 获取公告列表
*
* 通过 [asFlow] [asStream] 可以获取到*惰性*, 在从流中收集数据时才会请求服务器获取数据. 通常建议在 Kotlin 使用协程的 [asFlow], Java 使用 [asStream].
*
* 若要获取全部公告列表, 可使用 [toList].
*
* ## 发布群公告
*
* 查看 [Announcement]
*
* @since 2.7
*/
@JvmBlockingBridge
@NotStableForInheritance
public actual interface Announcements {
/**
* 创建一个能获取该群内所有群公告列表的 [Flow]. [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] [Flow] [收集][Flow.collect].
*/
public actual suspend fun asFlow(): Flow<OnlineAnnouncement>
/**
* 创建一个能获取该群内所有群公告列表的 [Stream]. [Stream] 被使用时才会分页下载 [OnlineAnnouncement].
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] [Stream] [收集][Stream.collect].
*
* 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [asFlow], 因此不建议在 Kotlin 使用. Kotlin 请使用 [asFlow].
*/
public fun asStream(): Stream<OnlineAnnouncement>
/**
* 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取并返回已经成功获取到的 [OfflineAnnouncement] 列表.
*
* @return 此时刻的群公告只读列表.
*/
public actual suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList()
/**
* 删除一条群公告. 需要管理员权限. 使用 [OnlineAnnouncement.delete] 与此方法效果相同.
*
* @param fid 公告的 [OnlineAnnouncement.fid]
* @return 成功返回 `true`, 群公告不存在时返回 `false`
*
* @throws PermissionDeniedException 当没有权限时抛出
* @throws IllegalStateException 当协议异常时抛出
*
* @see OnlineAnnouncement.delete
*/
public actual suspend fun delete(fid: String): Boolean
/**
* 获取一条群公告.
* @param fid 公告的 [OnlineAnnouncement.fid]
* @return 返回 `null` 表示不存在该 [fid] 的群公告
* @throws IllegalStateException 当协议异常时抛出
*/
public actual suspend fun get(fid: String): OnlineAnnouncement?
/**
* 在该群发布群公告并获得 [OnlineAnnouncement], 需要管理员权限. 发布公告后群内将会出现 "有新公告" 系统提示.
* @throws PermissionDeniedException 当没有权限时抛出
* @throws IllegalStateException 当协议异常时抛出
* @see Announcement.publishTo
*/
public actual suspend fun publish(announcement: Announcement): OnlineAnnouncement
/**
* 上传资源作为群公告图片. 返回值可用于 [AnnouncementParameters.image].
*
* **注意**: 需要由调用方[关闭][ExternalResource.close] [resource].
* @throws IllegalStateException 当协议异常时抛出
*/
public actual suspend fun uploadImage(resource: ExternalResource): AnnouncementImage
}

View File

@ -0,0 +1,11 @@
/*
* 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

@ -12,6 +12,8 @@ package net.mamoe.mirai.internal.utils
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.io.core.Input
import kotlinx.io.streams.asInput
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import java.io.Closeable import java.io.Closeable
import java.io.InputStream import java.io.InputStream
@ -96,6 +98,8 @@ internal abstract class ExternalResourceHolder : Closeable {
internal interface ExternalResourceInternal : ExternalResource { internal interface ExternalResourceInternal : ExternalResource {
val holder: ExternalResourceHolder val holder: ExternalResourceHolder
override fun input(): Input = inputStream().asInput()
} }
internal class ExternalResourceImplByFile( internal class ExternalResourceImplByFile(
@ -157,6 +161,10 @@ internal class ExternalResourceImplByByteArray(
get() = data//.clone() get() = data//.clone()
override fun inputStream(): InputStream = data.inputStream() override fun inputStream(): InputStream = data.inputStream()
override fun input(): Input {
return data.inputStream().asInput()
}
override fun close() { override fun close() {
kotlin.runCatching { closed.complete(Unit) } kotlin.runCatching { closed.complete(Unit) }
} }

View File

@ -0,0 +1,10 @@
/*
* 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

@ -0,0 +1,39 @@
/*
* 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.spi
import net.mamoe.mirai.utils.MiraiLogger
import java.util.*
import kotlin.reflect.KClass
internal actual class SPIServiceLoader<T : BaseService> actual constructor(
private val defaultService: T,
private val serviceType: KClass<T>
) {
actual var service: T = defaultService
actual fun reload() {
val loader = ServiceLoader.load(serviceType.java)
service = loader.minByOrNull { it.priority } ?: defaultService
}
init {
reload()
}
actual companion object {
actual val SPI_SERVICE_LOADER_LOGGER: MiraiLogger by lazy {
MiraiLogger.Factory.create(SPIServiceLoader::class, "spi-service-loader")
}
}
}

View File

@ -0,0 +1,257 @@
/*
* 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 kotlinx.atomicfu.atomic
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.io.errors.IOException
import net.mamoe.mirai.internal.utils.ExternalResourceHolder
import net.mamoe.mirai.internal.utils.ExternalResourceLeakObserver
import net.mamoe.mirai.internal.utils.detectFileTypeAndClose
import net.mamoe.mirai.utils.AbstractExternalResource.ResourceCleanCallback
import java.io.InputStream
/**
* 一个实现了基本方法的外部资源
*
* ## 实现
*
* [AbstractExternalResource] 实现了大部分必要的方法,
* 只有 [ExternalResource.inputStream], [ExternalResource.size] 还未实现
*
* 其中 [ExternalResource.inputStream] 要求每次读取的内容都是一致的
*
* Example:
* ```
* class MyCustomExternalResource: AbstractExternalResource() {
* override fun inputStream0(): InputStream = FileInputStream("/test.txt")
* override val size: Long get() = File("/test.txt").length()
* }
* ```
*
* ## 资源释放
*
* 如同 mirai 内置的 [ExternalResource] 实现一样,
* [AbstractExternalResource] 也会被注册进入资源泄露监视器
* (即意味着 [AbstractExternalResource] 也要求手动关闭)
*
* 为了确保逻辑正确性, [AbstractExternalResource] 不允许覆盖其 [close] 方法,
* 必须在构造 [AbstractExternalResource] 的时候给定一个 [ResourceCleanCallback] 以进行资源释放
*
* 对于 [ResourceCleanCallback], 有以下要求
*
* - 没有对 [AbstractExternalResource] 的访问 (即没有 [AbstractExternalResource] 的任何引用)
*
* Example:
* ```
* class MyRes(
* cleanup: ResourceCleanCallback,
* val delegate: Closable,
* ): AbstractExternalResource(cleanup) {
* }
*
* // 错误, 该写法会导致 Resource 永远也不会被自动释放
* lateinit var myRes: MyRes
* val cleanup = ResourceCleanCallback {
* myRes.delegate.close()
* }
* myRes = MyRes(cleanup, fetchDelegate())
*
* // 正确
* val delegate: Closable
* val cleanup = ResourceCleanCallback {
* delegate.close()
* }
* val myRes = MyRes(cleanup, delegate)
* ```
*
* @since 2.9
*
* @see ExternalResource
* @see AbstractExternalResource.setResourceCleanCallback
* @see AbstractExternalResource.registerToLeakObserver
*/
@Suppress("MemberVisibilityCanBePrivate")
public abstract class AbstractExternalResource
@JvmOverloads
public constructor(
displayName: String? = null,
cleanup: ResourceCleanCallback? = null,
) : ExternalResource {
public constructor(
cleanup: ResourceCleanCallback? = null,
) : this(null, cleanup)
public fun interface ResourceCleanCallback {
@Throws(IOException::class)
public fun cleanup()
}
override val md5: ByteArray by lazy { inputStream().md5() }
override val sha1: ByteArray by lazy { inputStream().sha1() }
override val formatName: String by lazy {
inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME
}
private val leakObserverRegistered = atomic(false)
/**
* 注册 [ExternalResource] 资源泄露监视器
*
* 受限于类继承构造器调用顺序, [AbstractExternalResource] 无法做到在完成初始化后马上注册监视器
*
* 该方法以允许 实现类 在完成初始化后直接注册资源监视器以避免意外的资源泄露
*
* 在不调用本方法的前提下, 如果没有相关的资源访问操作, `this` 可能会被意外泄露
*
* 正确示例:
* ```
* // Kotlin
* public class MyResource: AbstractExternalResource() {
* init {
* val res: SomeResource
* // 一些资源初始化
* registerToLeakObserver()
* setResourceCleanCallback(Releaser(res))
* }
*
* private class Releaser(
* private val res: SomeResource,
* ) : AbstractExternalResource.ResourceCleanCallback {
* override fun cleanup() = res.close()
* }
* }
*
* // Java
* public class MyResource extends AbstractExternalResource {
* public MyResource() throws IOException {
* SomeResource res;
* // 一些资源初始化
* registerToLeakObserver();
* setResourceCleanCallback(new Releaser(res));
* }
*
* private static class Releaser implements ResourceCleanCallback {
* private final SomeResource res;
* Releaser(SomeResource res) { this.res = res; }
*
* public void cleanup() throws IOException { res.close(); }
* }
* }
* ```
*
* @see setResourceCleanCallback
*/
protected fun registerToLeakObserver() {
// 用户自定义 AbstractExternalResource 也许会在 <init> 的时候失败
// 于是在第一次使用 ExternalResource 相关的函数的时候注册 LeakObserver
if (leakObserverRegistered.compareAndSet(expect = false, update = true)) {
ExternalResourceLeakObserver.register(this, holder)
}
}
/**
* 该方法用于告知 [AbstractExternalResource] 不需要注册资源泄露监视器
* **仅在我知道我在干什么的前提下调用此方法**
*
* 不建议取消注册监视器, 这可能带来意外的错误
*
* @see registerToLeakObserver
*/
protected fun dontRegisterLeakObserver() {
leakObserverRegistered.value = true
}
final override fun inputStream(): InputStream {
registerToLeakObserver()
return inputStream0()
}
protected abstract fun inputStream0(): InputStream
/**
* 修改 `this` 的资源释放回调
* **仅在我知道我在干什么的前提下调用此方法**
*
* ```
* class MyRes {
* // region kotlin
*
* private inner class Releaser : ResourceCleanCallback
*
* private class NotInnerReleaser : ResourceCleanCallback
*
* init {
* // 错误, 内部类, Releaser 存在对 MyRes 的引用
* setResourceCleanCallback(Releaser())
* // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于编译器
* setResourceCleanCallback(object : ResourceCleanCallback {})
* // 正确, 无 inner 修饰, 等同于 java 的 private static class
* setResourceCleanCallback(NotInnerReleaser(directResource))
* }
*
* // endregion kotlin
*
* // region java
*
* private class Releaser implements ResourceCleanCallback {}
* private static class StaticReleaser implements ResourceCleanCallback {}
*
* MyRes() {
* // 错误, 内部类, 存在对 MyRes 的引用
* setResourceCleanCallback(new Releaser());
* // 错误, 匿名对象, 可能存在对 MyRes 的引用, 取决于 javac
* setResourceCleanCallback(new ResourceCleanCallback() {});
* // 正确
* setResourceCleanCallback(new StaticReleaser(directResource));
* }
*
* // endregion java
* }
* ```
*
* @see registerToLeakObserver
*/
protected fun setResourceCleanCallback(cleanup: ResourceCleanCallback?) {
holder.cleanup = cleanup
}
private class UsrCustomResHolder(
@JvmField var cleanup: ResourceCleanCallback?,
private val resourceName: String,
) : ExternalResourceHolder() {
override val closed: Deferred<Unit> = CompletableDeferred()
override fun closeImpl() {
cleanup?.cleanup()
}
// display on logger of ExternalResourceLeakObserver
override fun toString(): String = resourceName
}
private val holder = UsrCustomResHolder(cleanup, displayName ?: buildString {
append("ExternalResourceHolder<")
append(this@AbstractExternalResource.javaClass.name)
append('@')
append(System.identityHashCode(this@AbstractExternalResource))
append('>')
})
final override val closed: Deferred<Unit> get() = holder.closed.also { registerToLeakObserver() }
@Throws(IOException::class)
final override fun close() {
holder.close()
}
}

View File

@ -0,0 +1,656 @@
/*
* 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:Suppress("unused", "DEPRECATION_ERROR", "EXPOSED_SUPER_CLASS", "MemberVisibilityCanBePrivate")
@file:JvmMultifileClass
@file:JvmName("Utils")
package net.mamoe.mirai.utils
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.Json
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotFactory
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.utils.DeviceInfo.Companion.loadAsDeviceInfo
import java.io.File
import java.io.InputStream
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* [Bot] 配置. 用于 [BotFactory.newBot]
*
* Kotlin 使用方法:
* ```
* val bot = BotFactory.newBot(...) {
* // 在这里配置 Bot
*
* bogLoggerSupplier = { bot -> ... }
* fileBasedDeviceInfo()
* inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
* }
* ```
*
* Java 使用方法:
* ```java
* Bot bot = BotFactory.newBot(..., new BotConfiguration() {{
* setBogLoggerSupplier((Bot bot) -> { ... })
* fileBasedDeviceInfo()
* ...
* }})
* ```
*/
@Suppress("PropertyName")
public actual open class BotConfiguration { // open for Java
/**
* 工作目录. 默认为 "."
*/
public var workingDir: File = File(".")
/**
* Json 序列化器, 使用 'kotlinx.serialization'
*/
@MiraiExperimentalApi
@Deprecated(
"Changing serial format is going to be forbidden. Deprecated for removal. ",
level = DeprecationLevel.ERROR
)
@DeprecatedSinceMirai(errorSince = "2.11") // was experimental
public var json: Json = kotlin.runCatching {
Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = true
}
}.getOrElse {
@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
Json {}
}
///////////////////////////////////////////////////////////////////////////
// Coroutines
///////////////////////////////////////////////////////////////////////////
/** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
public actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/**
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
*
* Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job]
*
* 用例:
* ```
* coroutineScope {
* val bot = Bot(...) {
* inheritCoroutineContext()
* }
* bot.login()
* } // coroutineScope 会等待 Bot 退出
* ```
*
*
* **注意**: `bot.cancel` 时将会让父 [Job] 也被 cancel.
* ```
* coroutineScope { // this: CoroutineScope
* launch {
* while(isActive) {
* delay(500)
* println("I'm alive")
* }
* }
*
* val bot = Bot(...) {
* inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
* }
* bot.login()
* bot.cancel() // 取消了整个 `coroutineScope`, 因此上文不断打印 `"I'm alive"` 的协程也会被取消.
* }
* ```
*
* 因此, 此函数尤为适合在 `suspend fun main()` 中使用, 它能阻止主线程退出:
* ```
* suspend fun main() {
* val bot = Bot() {
* inheritCoroutineContext()
* }
* bot.eventChannel.subscribe { ... }
*
* // 主线程不会退出, 直到 Bot 离线.
* }
* ```
*
* 简言之,
* - 若想让 [Bot] 作为 '守护进程' 运行, 则无需调用 [inheritCoroutineContext].
* - 若想让 [Bot] 依赖于当前协程, 让当前协程等待 [Bot] 运行, 则使用 [inheritCoroutineContext]
*
* @see parentCoroutineContext
*/
@JvmSynthetic
@ConfigurationDsl
public actual suspend inline fun inheritCoroutineContext() {
parentCoroutineContext = coroutineContext
}
///////////////////////////////////////////////////////////////////////////
// Connection
///////////////////////////////////////////////////////////////////////////
/** 连接心跳包周期. 过长会导致被服务器断开连接. */
public actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
/**
* 状态心跳包周期. 过长会导致掉线.
* 该值会在登录时根据服务器下发的配置自动进行更新.
* @since 2.6
* @see heartbeatStrategy
*/
public actual var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
/**
* 心跳策略.
* @since 2.6.3
*/
public actual var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
/**
* 心跳策略.
* @since 2.6.3
*/
public actual enum class HeartbeatStrategy {
/**
* 使用 2.6.0 增加的*状态心跳* (Stat Heartbeat). 通常推荐这个模式.
*
* 该模式大多数情况下更稳定. 但有些账号使用这个模式时会遇到一段时间后发送消息成功但客户端不可见的问题.
*/
STAT_HB,
/**
* 不发送状态心跳, 而是发送*切换在线状态* (可能会导致频繁的好友或客户端上线提示, 也可能产生短暂 (几秒) 发送消息不可见的问题).
*
* 建议在 [STAT_HB] 不可用时使用 [REGISTER].
*/
REGISTER,
/**
* 不主动维护会话. 多数账号会每 16 分钟掉线然后重连. 则会有短暂的不可用时间.
*
* 仅当 [STAT_HB] [REGISTER] 都造成无法接收等问题时使用.
* 同时请在 [https://github.com/mamoe/mirai/issues/1209] 提交问题.
*/
NONE;
}
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
public actual var heartbeatTimeoutMillis: Long = 5.secondsToMillis
/** 心跳失败后的第一次重连前的等待时间. */
@Deprecated(
"Useless since new network. Please just remove this.",
level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
/** 重连失败后, 继续尝试的每次等待时间 */
@Deprecated(
"Useless since new network. Please just remove this.",
level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public actual var reconnectPeriodMillis: Long = 5.secondsToMillis
/** 最多尝试多少次重连 */
public actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
/**
* 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
*
* 其他情况掉线都默认会自动重连, 详见 [BotOfflineEvent.reconnect]
*
* @since 2.1
*/
public actual var autoReconnectOnForceOffline: Boolean = false
/**
* 验证码处理器
*
* - Android 需要手动提供 [LoginSolver]
* - JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
*
* 详见 [LoginSolver.Default]
*
* @see LoginSolver
*/
public actual var loginSolver: LoginSolver? = LoginSolver.Default
/** 使用协议类型 */
public actual var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
public actual enum class MiraiProtocol {
/**
* Android 手机. 所有功能都支持.
*/
ANDROID_PHONE,
/**
* Android 平板.
*
* 注意: 不支持戳一戳事件解析
*/
ANDROID_PAD,
/**
* Android 手表.
*/
ANDROID_WATCH,
/**
* iPad - 来自MiraiGo
*
* @since 2.8
*/
IPAD,
/**
* MacOS - 来自MiraiGo
*
* @since 2.8
*/
MACOS,
}
/**
* Highway 通道上传图片, 语音, 文件等资源时的协程数量.
*
* 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
* 默认 [CPU 核心数][Runtime.availableProcessors].
*
* @since 2.2
*/
public actual var highwayUploadCoroutineCount: Int = Runtime.getRuntime().availableProcessors()
/**
* 设置 [autoReconnectOnForceOffline] `true`, 即在被挤下线时自动重连.
* @since 2.1
*/
@ConfigurationDsl
public actual fun autoReconnectOnForceOffline() {
autoReconnectOnForceOffline = true
}
///////////////////////////////////////////////////////////////////////////
// Device
///////////////////////////////////////////////////////////////////////////
@JvmField
internal actual var accountSecrets: Boolean = true
/**
* 禁止保存 `account.secrets`.
*
* `account.secrets` 保存账号的会话信息
* 它可加速登录过程也可能可以减少出现验证码的次数如果遇到一段时间后无法接收消息通知等同步问题时可尝试禁用
*
* @since 2.11
*/
public actual fun disableAccountSecretes() {
accountSecrets = false
}
/**
* 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
* @see fileBasedDeviceInfo 使用指定文件存储设备信息
* @see randomDeviceInfo 使用随机设备信息
*/
public actual var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
/**
* 使用随机设备信息.
*
* @see deviceInfo
*/
@ConfigurationDsl
public actual fun randomDeviceInfo() {
deviceInfo = null
}
/**
* 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
*
* @see deviceInfo
*/
@ConfigurationDsl
public actual fun loadDeviceInfoJson(json: String) {
deviceInfo = {
this.json.decodeFromString(DeviceInfo.serializer(), json)
}
}
/**
* 使用文件存储设备信息.
*
* 此函数只在 JVM Android 有效. 在其他平台将会抛出异常.
* @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
* @see deviceInfo
*/
@JvmOverloads
@ConfigurationDsl
public actual fun fileBasedDeviceInfo(filepath: String) {
deviceInfo = getFileBasedDeviceInfoSupplier { workingDir.resolve(filepath) }
}
///////////////////////////////////////////////////////////////////////////
// Logging
///////////////////////////////////////////////////////////////////////////
/**
* 日志记录器
*
* - 默认打印到标准输出, 通过 [MiraiLogger.create]
* - 忽略所有日志: [noBotLog]
* - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
* - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
*
* @see MiraiLogger
*/
public actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
}
/**
* 网络层日志构造器
*
* - 默认打印到标准输出, 通过 [MiraiLogger.create]
* - 忽略所有日志: [noNetworkLog]
* - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
* - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
*
* @see MiraiLogger
*/
public actual var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
}
/**
* 重定向 [网络日志][networkLoggerSupplier] 到指定目录. 若目录不存在将会自动创建 ([File.mkdirs])
* 默认目录路径为 "$workingDir/logs/".
* @see DirectoryLogger
* @see redirectNetworkLogToDirectory
*/
@JvmOverloads
@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
@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
@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
@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) }
}
/**
* 不显示网络日志. 不推荐.
* @see networkLoggerSupplier 更多日志处理方式
*/
@ConfigurationDsl
public actual fun noNetworkLog() {
networkLoggerSupplier = { _ -> SilentLogger }
}
/**
* 不显示 [Bot] 日志. 不推荐.
* @see botLoggerSupplier 更多日志处理方式
*/
@ConfigurationDsl
public actual fun noBotLog() {
botLoggerSupplier = { _ -> SilentLogger }
}
/**
* 是否显示过于冗长的事件日志
*
* 默认为 `false`
*
* @since 2.8
*/
public actual var isShowingVerboseEventLog: Boolean = false
///////////////////////////////////////////////////////////////////////////
// Cache
//////////////////////////////////////////////////////////////////////////
/**
* 缓存数据目录, 相对于 [workingDir].
*
* 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
* 默认启用的缓存可以加快登录过程.
*
* 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
* - 联系人列表
* - 登录服务器列表
* - 资源服务秘钥
*
* 其他内容如通过 [InputStream] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
*
* @since 2.4
*/
public var cacheDir: File = File("cache")
/**
* 联系人信息缓存配置. 将会保存在 [cacheDir] `contacts` 目录
* @since 2.4
*/
public actual var contactListCache: ContactListCache = ContactListCache()
/**
* 联系人信息缓存配置
* @see contactListCache
* @see enableContactCache
* @see disableContactCache
* @since 2.4
*/
public actual class ContactListCache {
/**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/
public actual var saveIntervalMillis: Long = 60_000
/**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/ // was @ExperimentalTime before 2.9
public actual inline var saveInterval: Duration
@JvmSynthetic inline get() = saveIntervalMillis.milliseconds
@JvmSynthetic inline set(v) {
saveIntervalMillis = v.inWholeMilliseconds
}
/**
* 开启好友列表缓存.
*/
public actual var friendListCacheEnabled: Boolean = false
/**
* 开启群成员列表缓存.
*/
public actual var groupMemberListCacheEnabled: Boolean = false
}
/**
* 配置 [ContactListCache]
* ```
* contactListCache {
* saveIntervalMillis = 30_000
* friendListCacheEnabled = true
* }
* ```
* @since 2.4
*/
@JvmSynthetic
public actual inline fun contactListCache(action: ContactListCache.() -> Unit) {
action.invoke(this.contactListCache)
}
/**
* 禁用好友列表和群成员列表的缓存.
* @since 2.4
*/
@ConfigurationDsl
public actual fun disableContactCache() {
contactListCache.friendListCacheEnabled = false
contactListCache.groupMemberListCacheEnabled = false
}
/**
* 启用好友列表和群成员列表的缓存.
* @since 2.4
*/
@ConfigurationDsl
public actual fun enableContactCache() {
contactListCache.friendListCacheEnabled = true
contactListCache.groupMemberListCacheEnabled = true
}
/**
* 登录缓存.
*
* 开始后在密码登录成功时会保存秘钥等信息, 在下次启动时通过这些信息登录, 而不提交密码.
* 可以减少验证码出现的频率.
*
* 秘钥信息会由密码加密保存. 如果秘钥过期, 则会进行普通密码登录.
*
* 默认 `true` (开启).
*
* @since 2.6
*/
public actual var loginCacheEnabled: Boolean = true
///////////////////////////////////////////////////////////////////////////
// Misc
///////////////////////////////////////////////////////////////////////////
@Suppress("DuplicatedCode")
public actual fun copy(): BotConfiguration {
return BotConfiguration().also { new ->
// To structural order
new.workingDir = workingDir
@Suppress("DEPRECATION_ERROR")
new.json = json
new.parentCoroutineContext = parentCoroutineContext
new.heartbeatPeriodMillis = heartbeatPeriodMillis
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
new.heartbeatStrategy = heartbeatStrategy
new.reconnectionRetryTimes = reconnectionRetryTimes
new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
new.loginSolver = loginSolver
new.protocol = protocol
new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
new.accountSecrets = accountSecrets
new.deviceInfo = deviceInfo
new.botLoggerSupplier = botLoggerSupplier
new.networkLoggerSupplier = networkLoggerSupplier
new.cacheDir = cacheDir
new.contactListCache = contactListCache
new.convertLineSeparator = convertLineSeparator
new.isShowingVerboseEventLog = isShowingVerboseEventLog
}
}
/**
* 是否处理接受到的特殊换行符, 默认为 `true`
*
* - 若为 `true`, 会将收到的 `CRLF(\r\n)` `CR(\r)` 替换为 `LF(\n)`
* - 若为 `false`, 则不做处理
*
* @since 2.4
*/
@get:JvmName("isConvertLineSeparator")
public actual var convertLineSeparator: Boolean = true
/** 标注一个配置 DSL 函数 */
@Target(AnnotationTarget.FUNCTION)
@DslMarker
public actual annotation class ConfigurationDsl
public actual companion object {
/** 默认的配置实例. 可以进行修改 */
@JvmStatic
public actual val Default: BotConfiguration = BotConfiguration()
}
}
internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
return {
@Suppress("DEPRECATION_ERROR")
file().loadAsDeviceInfo(json)
}
}

View File

@ -0,0 +1,142 @@
/*
* 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 kotlinx.io.core.toByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import java.io.File
import kotlin.random.Random
@Serializable
public actual class DeviceInfo actual constructor(
public actual val display: ByteArray,
public actual val product: ByteArray,
public actual val device: ByteArray,
public actual val board: ByteArray,
public actual val brand: ByteArray,
public actual val model: ByteArray,
public actual val bootloader: ByteArray,
public actual val fingerprint: ByteArray,
public actual val bootId: ByteArray,
public actual val procVersion: ByteArray,
public actual val baseBand: ByteArray,
public actual val version: Version,
public actual val simInfo: ByteArray,
public actual val osType: ByteArray,
public actual val macAddress: ByteArray,
public actual val wifiBSSID: ByteArray,
public actual val wifiSSID: ByteArray,
public actual val imsiMd5: ByteArray,
public actual val imei: String,
public actual val apn: ByteArray
) {
public actual val androidId: ByteArray get() = display
public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
init {
require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." }
}
@Transient
@MiraiInternalApi
public actual val guid: ByteArray = generateGuid(androidId, macAddress)
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // serializable
@Serializable
public actual class Version actual constructor(
public actual val incremental: ByteArray = "5891938".toByteArray(),
public actual val release: ByteArray = "10".toByteArray(),
public actual val codename: ByteArray = "REL".toByteArray(),
public actual val sdk: Int = 29
) {
/**
* @since 2.9
*/
actual override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Version) return false
if (!incremental.contentEquals(other.incremental)) return false
if (!release.contentEquals(other.release)) return false
if (!codename.contentEquals(other.codename)) return false
if (sdk != other.sdk) return false
return true
}
/**
* @since 2.9
*/
actual override fun hashCode(): Int {
var result = incremental.contentHashCode()
result = 31 * result + release.contentHashCode()
result = 31 * result + codename.contentHashCode()
result = 31 * result + sdk
return result
}
}
public actual companion object {
internal actual val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
/**
* 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.
*/
@JvmOverloads
@JvmStatic
@JvmName("from")
public fun File.loadAsDeviceInfo(
json: Json = DeviceInfoManager.format
): DeviceInfo {
if (!this.exists() || this.length() == 0L) {
return random().also {
this.writeText(DeviceInfoManager.serialize(it, json))
}
}
return DeviceInfoManager.deserialize(this.readText(), json)
}
/**
* 生成随机 [DeviceInfo]
*
* @since 2.0
*/
@JvmStatic
public actual fun random(): DeviceInfo = random(Random.Default)
/**
* 使用特定随机数生成器生成 [DeviceInfo]
*
* @since 2.9
*/
@JvmStatic
public actual fun random(random: Random): DeviceInfo {
return DeviceInfoCommonImpl.randomDeviceInfo(random)
}
}
/**
* @since 2.9
*/
@Suppress("DuplicatedCode")
actual override fun equals(other: Any?): Boolean {
return DeviceInfoCommonImpl.equalsImpl(this, other)
}
/**
* @since 2.9
*/
actual override fun hashCode(): Int {
return DeviceInfoCommonImpl.hashCodeImpl(this)
}
}

View File

@ -0,0 +1,623 @@
/*
* 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 kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.io.core.Input
import kotlinx.io.errors.IOException
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.internal.utils.inputStream
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.InputStream
import java.io.RandomAccessFile
/**
* 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
*
* [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
*
* # 创建
* - [File.toExternalResource]
* - [RandomAccessFile.toExternalResource]
* - [ByteArray.toExternalResource]
* - [InputStream.toExternalResource]
*
* ## Kotlin 获得和使用 [ExternalResource] 实例
*
* ```
* file.toExternalResource().use { resource -> // 安全地使用资源
* contact.uploadImage(resource) // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
* }
* ```
*
* 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
*
* ```
* inputStream.use { input -> // 安全地使用 InputStream
* input.toExternalResource().use { resource -> // 安全地使用资源
* contact.uploadImage(resource) // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
* }
* }
* ```
*
* ## Java 获得和使用 [ExternalResource] 实例
*
* ```
* try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
* contact.uploadImage(resource); // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
* }
* ```
*
* 注意, 若使用 [InputStream], 必须手动关闭 [InputStream]. 一种使用情况示例:
*
* ```java
* try (InputStream stream = ...) { // 安全地使用 InputStream
* try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
* contact.uploadImage(resource); // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
* }
* }
* ```
*
* # 释放
*
* [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
*/
@MiraiExperimentalApi
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] 前无法再次打开流.
*
* 关闭此流不会关闭 [ExternalResource].
* @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
*
* @since SINCE_NATIVE_TARGET
*/
@MiraiExperimentalApi
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)")
) // deprecated since 2.8.0-RC
@JvmStatic
@JvmBlockingBridge
@JvmOverloads
@DeprecatedSinceMirai(warningSince = "2.8")
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)")
) // deprecated since 2.8.0-RC
@JvmStatic
@JvmBlockingBridge
@JvmName("sendAsFile")
@JvmOverloads
@DeprecatedSinceMirai(warningSince = "2.8")
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,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils

View File

@ -0,0 +1,302 @@
/*
* 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("Utils")
package net.mamoe.mirai.utils
import net.mamoe.mirai.utils.*
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.setDefaultLoggerCreator] 全局覆盖日志.
*
* ## 实现或使用 [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 loadService(Factory::class, { DefaultFactory() })
}
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 actual val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
/**
* 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
"Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory",
level = DeprecationLevel.ERROR
) // deprecated since 2.7
@JvmStatic
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
public actual fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
DefaultFactoryOverrides.override { _, identity -> creator(identity) }
}
/**
* 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
*
* @see setDefaultLoggerCreator
*/
@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 actual 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
/**
* 随从. this 中调用所有方法后都应继续往 [follower] 传递调用.
* [follower] 的存在可以让一次日志被多个日志记录器记录.
*
* 一般不建议直接修改这个属性. 请通过 [plus] 来连接两个日志记录器.
* : `val logger = bot.logger + MyLogger()`
* 当调用 `logger.info()` , `bot.logger` 会首先记录, `MyLogger` 会随后记录.
*
* 当然, 多个 logger 也可以加在一起: `val logger = bot.logger + MynLogger() + MyLogger2()`
*/
@Suppress("UNUSED_PARAMETER")
@Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public actual 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 =
priority.correspondingFunction(this, message, e)
/**
* 添加一个 [follower], 返回 [follower]
* 它只会把 `this` 的属性 [MiraiLogger.follower] 修改为这个函数的参数 [follower], 然后返回这个参数.
* [MiraiLogger.follower] 已经有值, 则会替换掉这个值.
* ```
* +------+ +----------+ +----------+ +----------+
* | base | <-- | follower | <-- | follower | <-- | follower |
* +------+ +----------+ +----------+ +----------+
* ```
*
* @return [follower]
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public actual operator fun <T : MiraiLogger> plus(follower: T): T = follower
}
internal object DefaultFactoryOverrides {
var override: ((requester: Class<*>, identity: String?) -> MiraiLogger)? =
null // 支持 LoggerAdapters 以及兼容旧版本
@JvmStatic
fun override(lambda: (requester: Class<*>, identity: String?) -> MiraiLogger) {
override = lambda
}
@JvmStatic
fun clearOverride() {
override = null
}
}
internal class DefaultFactory : MiraiLogger.Factory {
override fun create(requester: Class<*>, identity: String?): MiraiLogger {
val override = DefaultFactoryOverrides.override
return if (override != null) override(requester, identity) else PlatformLogger(
identity ?: requester.kotlin.simpleName ?: requester.simpleName
)
}
}

View File

@ -0,0 +1,667 @@
/*
* 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:Suppress("unused", "DEPRECATION")
@file:JvmBlockingBridge
package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.toList
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
import java.io.File
/**
* 表示一个远程文件或目录.
*
* [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的.
* 意味着操作的结果会因文件或目录在服务器中的状态变化而变化.
*
* [File] 类似, [RemoteFile] 是不可变的. [renameTo] [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性.
*
* ## 文件操作
*
* 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件.
*
* 示例:
* ```
* val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例
* val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例
*
*
* val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载
*
*
* val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息.
* group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送.
*
* file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本.
*
*
* // 要直接上传文件, 也可以简单地使用任一:
* group.uploadFile("/foo.txt", resource) // Kotlin
* resource.uploadAsFileTo(group, "/foo.txt") // Kotlin
* FileSupported.uploadFile(group, "/foo.txt", resource"); // Java
* ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java
* ```
*
* ## 目录操作
* [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录.
* ```
* val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例
*
* if (dir.exists()) { // 判断目录是否存在
* // ...
* }
*
* dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表.
* dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表.
* ```
*
* 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" "/xxx/foo.txt", "/xxx/xxx/foo.txt" 不受支持.
*
* ## 文件名和目录名可重复
*
* 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠.
*
* 这个特性带来的行为有:
* - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件.
* - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器.
*
* 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id].
*
* 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` [id].
* 服务器可以通过 [id] 准确定位重名文件中的某一个.
* 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件.
*
* 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件.
*
* @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口.
* @see FileSupported
* @since 2.5
*/
@Deprecated(
"Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
@NotStableForInheritance
public actual interface RemoteFile {
/**
* 文件名或目录名.
*/
public actual val name: String
/**
* 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
*/
public actual val id: String?
/**
* 标准的绝对路径, 起始字符为 '/'. `/foo/bar.txt`.
*
* 根目录路径为 [ROOT_PATH]
*/
public actual val path: String
/**
* 获取父目录, [RemoteFile] 表示根目录时返回 `null`
*/
public actual val parent: RemoteFile?
/**
* 此文件所属的群或好友
*/
public actual val contact: FileSupported
/**
* [RemoteFile] 表示一个文件时返回 `true`.
*/
public actual suspend fun isFile(): Boolean
/**
* [RemoteFile] 表示一个目录时返回 `true`.
*/
public actual suspend fun isDirectory(): Boolean = !isFile()
/**
* 获取文件长度. [RemoteFile] 表示一个目录时行为不确定.
*/
public actual suspend fun length(): Long
public actual class FileInfo @MiraiInternalApi actual constructor(
/**
* 文件或目录名.
*/
public actual val name: String,
/**
* 唯一识别标识.
*/
public actual val id: String,
/**
* 标准绝对路径.
*/
public actual val path: String,
/**
* 文件长度 (大小) bytes, 目录的 [length] 0.
*/
public actual val length: Long,
/**
* 下载次数. 目录没有下载次数, 此属性总是 `0`.
*/
public actual val downloadTimes: Int,
/**
* 上传者 ID. 目录没有上传者, 此属性总是 `0`.
*/
public actual val uploaderId: Long,
/**
* 上传的时间. 目录没有上传时间, 此属性总是 `0`.
*/
public actual val uploadTime: Long,
/**
* 上次修改时间. 时间戳秒.
*/
public actual val lastModifyTime: Long,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
) {
/**
* 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile].
*/
public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
}
/**
* 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
*/
public actual suspend fun getInfo(): FileInfo?
/**
* 当文件或目录存在时返回 `true`.
*/
public actual suspend fun exists(): Boolean
/**
* @return [path]
*/
public actual override fun toString(): String
///////////////////////////////////////////////////////////////////////////
// resolve
///////////////////////////////////////////////////////////////////////////
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: String): RemoteFile
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
*
* @param relative 相对路径. [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: RemoteFile): RemoteFile
/**
* 获取该目录下的 ID [id] 的文件, [deep] `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
* @see resolve
*/
public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
/**
* 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null`
* @see resolve
*/
public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
* 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: String): RemoteFile
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
* 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
*
* @param relative [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: RemoteFile): RemoteFile
///////////////////////////////////////////////////////////////////////////
// operations
///////////////////////////////////////////////////////////////////////////
/**
* 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*/
public actual suspend fun delete(): Boolean
/**
* 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
* 操作非 Bot 自己上传的文件时需要管理员权限.
*
* [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*/
public actual suspend fun renameTo(name: String): Boolean
/**
* 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*
* [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*
* **注意**: [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如:
* ```
* val root = group.filesRoot
* root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt"
* root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同.
* root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”相当于重命名文件
* ```
*
* @param target 目标文件位置.
*/
public actual suspend fun moveTo(target: RemoteFile): Boolean
/**
* 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*
* [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*
* **已弃用:** [path] 是绝对路径时, 这个函数运行正常;
* 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败.
*
* 使用参数为 [RemoteFile] [moveTo] 代替.
*
* @suppress 2.6 弃用. 请使用 [moveTo]
*/
@Deprecated(
"Use moveTo(RemoteFile) instead.",
replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"),
level = DeprecationLevel.ERROR
) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
public actual suspend fun moveTo(path: String): Boolean {
// Impl notes:
// if `path` is absolute, this works as intended.
// if not, `resolve(path)` will be a child path from this dir and fails always.
return moveTo(resolve(path))
}
/**
* 创建目录. 目录已经存在或无管理员权限时返回 `false`.
*
* 创建后 [isDirectory] 也不一定会返回 `true`.
* [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] [isDirectory] 结果取决于服务器.
*/
public actual suspend fun mkdir(): Boolean
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyFlow].
*
* 返回的 [Flow] **, 只会在被需要的时候向服务器查询.
*/
public actual suspend fun listFiles(): Flow<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回空迭代器.
* @param lazy `true` 时惰性获取, `false` 时立即获取全部文件列表.
*/
@JavaFriendlyAPI
public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList].
*/
public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
/**
* 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
*/
public actual suspend fun toMessage(): FileMessage?
///////////////////////////////////////////////////////////////////////////
// upload & download
///////////////////////////////////////////////////////////////////////////
/**
* 上传进度回调, 可供前端使用, 以提供进度显示.
* @see asProgressionCallback
*/
@Deprecated(
"Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile",
ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual interface ProgressionCallback {
/**
* 当上传开始时调用
*/
public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
/**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
*
* 提示: 可通过 [ExternalResource.size] 获取文件总大小.
*/
public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
/**
* 当上传成功时调用
*/
public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
/**
* 当上传以异常失败时调用
*/
public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
public actual companion object {
/**
* 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
*
* 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer] [SendChannel] .
* 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存.
*
* [closeOnFinish] `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel].
*
* 使用示例:
* ```
* val progress = Channel<Long>(Channel.BUFFERED)
*
* launch {
* // 每 3 秒发送一次上传进度百分比
* progress.receiveAsFlow().sample(3.seconds).collect { bytes ->
* group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数
* }
* }
*
* group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true))
* group.sendMessage("File uploaded successfully.")
* ```
*
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
*/
@JvmStatic
public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
return object : ProgressionCallback {
override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
trySend(downloadedSize)
}
override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
if (closeOnFinish) this@asProgressionCallback.close()
}
override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
if (closeOnFinish) this@asProgressionCallback.close(exception)
}
}
}
}
}
/**
* 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度.
*
* 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件.
* 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend].
*
* ## 已弃用
*
* 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息.
* 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作.
* 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替.
*
* 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250)
*
*
* **注意**: [resource] 仅表示资源数据, 而不带有文件名属性.
* [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例:
* ```
* group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录.
* group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录.
*
* val root = group.filesRoot
* root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt".
* root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt".
* ```
*
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @param callback 进度回调
* @throws IllegalStateException 该文件上传失败或权限不足时抛出
*/
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun upload(
resource: ExternalResource,
callback: ProgressionCallback?,
): FileMessage
/**
* 上传文件到 [RemoteFile.path] 表示的路径.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
/**
* 上传文件.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(file, callback)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun upload(
file: File,
callback: ProgressionCallback? = null,
): FileMessage = file.toExternalResource().use { upload(it, callback) }
/**
* 上传文件.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use sendFile instead.", ReplaceWith("this.uploadAndSend(file)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun upload(file: File): FileMessage = file.toExternalResource().use { upload(it) }
/**
* 上传文件并发送文件消息.
*
* [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件.
* 即使用 [resolve] [resolveSibling] 获取到的 [RemoteFile] [upload] 总是上传一个新文件,
* 而使用 [resolveById] [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件.
*
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see upload
*/
@MiraiExperimentalApi
public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
/**
* 上传文件并发送文件消息.
* @see uploadAndSend
*/
@MiraiExperimentalApi
public suspend fun uploadAndSend(file: File): MessageReceipt<Contact> =
file.toExternalResource().use { uploadAndSend(it) }
/**
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
*/
public actual suspend fun getDownloadInfo(): DownloadInfo?
public actual class DownloadInfo @MiraiInternalApi actual constructor(
/**
* @see RemoteFile.name
*/
public actual val filename: String,
/**
* @see RemoteFile.id
*/
public actual val id: String,
/**
* 标准绝对路径
* @see RemoteFile.path
*/
public actual val path: String,
/**
* HTTP or HTTPS URL
*/
public actual val url: String,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
) {
actual override fun toString(): String {
return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
"md5=${md5.toUHexString("")})"
}
}
public actual companion object {
/**
* 根目录路径
* @see RemoteFile.path
*/
public actual const val ROOT_PATH: String = "/"
/**
* 上传文件并获取文件消息, 但不发送.
*
* ## 已弃用
* [upload] 获取更多信息
*
* @param path 远程路径. 起始字符为 '/'. '/foo/bar.txt'
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see RemoteFile.upload
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Use sendFile instead.",
ReplaceWith(
"this.sendFile(path, resource, callback)",
"net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
),
level = DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun FileSupported.uploadFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
): FileMessage =
@Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
/**
* 上传文件并获取文件消息, 但不发送.
* ## 已弃用
* 阅读 [uploadFile] 获取更多信息.
*
* @param path 远程路径. 起始字符为 '/'. '/foo/bar.txt'
* @see RemoteFile.upload
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Use sendFile instead.",
ReplaceWith(
"this.sendFile(path, file, callback)",
"net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
),
level = DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public suspend fun FileSupported.uploadFile(
path: String,
file: File,
callback: ProgressionCallback? = null,
): FileMessage =
@Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(file, callback)
/**
* 上传文件并发送文件消息到相关 [FileSupported].
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see RemoteFile.uploadAndSend
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual suspend fun <C : FileSupported> C.sendFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
): MessageReceipt<C> =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
/**
* 上传文件并发送文件消息到相关 [FileSupported].
* @see RemoteFile.uploadAndSend
*/
@JvmStatic
@JvmOverloads
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public suspend fun <C : FileSupported> C.sendFile(
path: String,
file: File,
callback: ProgressionCallback? = null,
): MessageReceipt<C> =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(file, callback).sendTo(this)
}
}

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmName("FileLoggerKt") // bin-comp @file:JvmName("FileLoggerKt") // bin-comp

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.internal.utils package net.mamoe.mirai.internal.utils

View File

@ -0,0 +1,10 @@
/*
* 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

@ -0,0 +1,95 @@
/*
* 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.announcement
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.toList
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 表示一个群的公告列表 (管理器).
*
* ## 获取群公告
*
* ### 获取 [Announcements] 实例
*
* 只可以通过 [Group.announcements] 获取一个群的公告列表, [Announcements] 实例.
*
* ### 获取公告列表
*
* 通过 [asFlow] 可以获取到*惰性*, 在从流中收集数据时才会请求服务器获取数据.
*
* 若要获取全部公告列表, 可使用 [toList].
*
* ## 发布群公告
*
* 查看 [Announcement]
*
* @since 2.7
*/
@NotStableForInheritance
public actual interface Announcements {
/**
* 创建一个能获取该群内所有群公告列表的 [Flow]. [Flow] 被使用时才会分页下载 [OnlineAnnouncement].
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取, 不影响已经成功获取的 [OfflineAnnouncement] [Flow] [收集][Flow.collect].
*/
public actual suspend fun asFlow(): Flow<OnlineAnnouncement>
/**
* 获取所有群公告列表, 将全部 [OnlineAnnouncement] 都下载后再返回.
*
* 异常不会抛出, 只会记录到网络日志. 当获取发生异常时将会终止获取并返回已经成功获取到的 [OfflineAnnouncement] 列表.
*
* @return 此时刻的群公告只读列表.
*/
public actual suspend fun toList(): List<OnlineAnnouncement> = asFlow().toList()
/**
* 删除一条群公告. 需要管理员权限. 使用 [OnlineAnnouncement.delete] 与此方法效果相同.
*
* @param fid 公告的 [OnlineAnnouncement.fid]
* @return 成功返回 `true`, 群公告不存在时返回 `false`
*
* @throws PermissionDeniedException 当没有权限时抛出
* @throws IllegalStateException 当协议异常时抛出
*
* @see OnlineAnnouncement.delete
*/
public actual suspend fun delete(fid: String): Boolean
/**
* 获取一条群公告.
* @param fid 公告的 [OnlineAnnouncement.fid]
* @return 返回 `null` 表示不存在该 [fid] 的群公告
* @throws IllegalStateException 当协议异常时抛出
*/
public actual suspend fun get(fid: String): OnlineAnnouncement?
/**
* 在该群发布群公告并获得 [OnlineAnnouncement], 需要管理员权限. 发布公告后群内将会出现 "有新公告" 系统提示.
* @throws PermissionDeniedException 当没有权限时抛出
* @throws IllegalStateException 当协议异常时抛出
* @see Announcement.publishTo
*/
public actual suspend fun publish(announcement: Announcement): OnlineAnnouncement
/**
* 上传资源作为群公告图片. 返回值可用于 [AnnouncementParameters.image].
*
* **注意**: 需要由调用方[关闭][ExternalResource.close] [resource].
* @throws IllegalStateException 当协议异常时抛出
*/
public actual suspend fun uploadImage(resource: ExternalResource): AnnouncementImage
}

View File

@ -0,0 +1,10 @@
/*
* 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

@ -0,0 +1,35 @@
/*
* 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.spi
import net.mamoe.mirai.utils.MiraiLogger
import kotlin.reflect.KClass
internal actual class SPIServiceLoader<T : BaseService> actual constructor(
defaultService: T,
private val serviceType: KClass<T>
) {
actual var service: T = defaultService
actual fun reload() {
TODO("native")
}
init {
reload()
}
actual companion object {
actual val SPI_SERVICE_LOADER_LOGGER: MiraiLogger by lazy {
MiraiLogger.Factory.create(SPIServiceLoader::class, "spi-service-loader")
}
}
}

View File

@ -0,0 +1,554 @@
/*
* 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 kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.io.core.Input
import kotlinx.serialization.json.Json
import net.mamoe.mirai.Bot
import net.mamoe.mirai.BotFactory
import net.mamoe.mirai.event.events.BotOfflineEvent
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.coroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* [Bot] 配置. 用于 [BotFactory.newBot]
*
* Kotlin 使用方法:
* ```
* val bot = BotFactory.newBot(...) {
* // 在这里配置 Bot
*
* bogLoggerSupplier = { bot -> ... }
* fileBasedDeviceInfo()
* inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
* }
* ```
*
* Java 使用方法:
* ```java
* Bot bot = BotFactory.newBot(..., new BotConfiguration() {{
* setBogLoggerSupplier((Bot bot) -> { ... })
* fileBasedDeviceInfo()
* ...
* }})
* ```
*/
@Suppress("PropertyName")
public actual open class BotConfiguration { // open for Java
/**
* 工作目录. 默认为当前目录
*/
public var workingDir: String = "."
///////////////////////////////////////////////////////////////////////////
// Coroutines
///////////////////////////////////////////////////////////////////////////
/** 父 [CoroutineContext]. [Bot] 创建后会使用 [SupervisorJob] 覆盖其 [Job], 但会将这个 [Job] 作为父 [Job] */
public actual var parentCoroutineContext: CoroutineContext = EmptyCoroutineContext
/**
* 使用当前协程的 [coroutineContext] 作为 [parentCoroutineContext].
*
* Bot 将会使用一个 [SupervisorJob] 覆盖 [coroutineContext] 当前协程的 [Job], 并使用当前协程的 [Job] 作为父 [Job]
*
* 用例:
* ```
* coroutineScope {
* val bot = Bot(...) {
* inheritCoroutineContext()
* }
* bot.login()
* } // coroutineScope 会等待 Bot 退出
* ```
*
*
* **注意**: `bot.cancel` 时将会让父 [Job] 也被 cancel.
* ```
* coroutineScope { // this: CoroutineScope
* launch {
* while(isActive) {
* delay(500)
* println("I'm alive")
* }
* }
*
* val bot = Bot(...) {
* inheritCoroutineContext() // 使用 `coroutineScope` 的 Job 作为父 Job
* }
* bot.login()
* bot.cancel() // 取消了整个 `coroutineScope`, 因此上文不断打印 `"I'm alive"` 的协程也会被取消.
* }
* ```
*
* 因此, 此函数尤为适合在 `suspend fun main()` 中使用, 它能阻止主线程退出:
* ```
* suspend fun main() {
* val bot = Bot() {
* inheritCoroutineContext()
* }
* bot.eventChannel.subscribe { ... }
*
* // 主线程不会退出, 直到 Bot 离线.
* }
* ```
*
* 简言之,
* - 若想让 [Bot] 作为 '守护进程' 运行, 则无需调用 [inheritCoroutineContext].
* - 若想让 [Bot] 依赖于当前协程, 让当前协程等待 [Bot] 运行, 则使用 [inheritCoroutineContext]
*
* @see parentCoroutineContext
*/
@ConfigurationDsl
public actual suspend inline fun inheritCoroutineContext() {
parentCoroutineContext = coroutineContext
}
///////////////////////////////////////////////////////////////////////////
// Connection
///////////////////////////////////////////////////////////////////////////
/** 连接心跳包周期. 过长会导致被服务器断开连接. */
public actual var heartbeatPeriodMillis: Long = 60.secondsToMillis
/**
* 状态心跳包周期. 过长会导致掉线.
* 该值会在登录时根据服务器下发的配置自动进行更新.
* @since 2.6
* @see heartbeatStrategy
*/
public actual var statHeartbeatPeriodMillis: Long = 300.secondsToMillis
/**
* 心跳策略.
* @since 2.6.3
*/
public actual var heartbeatStrategy: HeartbeatStrategy = HeartbeatStrategy.STAT_HB
/**
* 心跳策略.
* @since 2.6.3
*/
public actual enum class HeartbeatStrategy {
/**
* 使用 2.6.0 增加的*状态心跳* (Stat Heartbeat). 通常推荐这个模式.
*
* 该模式大多数情况下更稳定. 但有些账号使用这个模式时会遇到一段时间后发送消息成功但客户端不可见的问题.
*/
STAT_HB,
/**
* 不发送状态心跳, 而是发送*切换在线状态* (可能会导致频繁的好友或客户端上线提示, 也可能产生短暂 (几秒) 发送消息不可见的问题).
*
* 建议在 [STAT_HB] 不可用时使用 [REGISTER].
*/
REGISTER,
/**
* 不主动维护会话. 多数账号会每 16 分钟掉线然后重连. 则会有短暂的不可用时间.
*
* 仅当 [STAT_HB] [REGISTER] 都造成无法接收等问题时使用.
* 同时请在 [https://github.com/mamoe/mirai/issues/1209] 提交问题.
*/
NONE;
}
/**
* 每次心跳时等待结果的时间.
* 一旦心跳超时, 整个网络服务将会重启 (将消耗约 1s). 除正在进行的任务 (如图片上传) 会被中断外, 事件和插件均不受影响.
*/
public actual var heartbeatTimeoutMillis: Long = 5.secondsToMillis
/** 心跳失败后的第一次重连前的等待时间. */
@Deprecated(
"Useless since new network. Please just remove this.",
level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public actual var firstReconnectDelayMillis: Long = 5.secondsToMillis
/** 重连失败后, 继续尝试的每次等待时间 */
@Deprecated(
"Useless since new network. Please just remove this.",
level = DeprecationLevel.HIDDEN
) // deprecated since 2.7, error since 2.8
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.8", hiddenSince = "2.10")
public actual var reconnectPeriodMillis: Long = 5.secondsToMillis
/** 最多尝试多少次重连 */
public actual var reconnectionRetryTimes: Int = Int.MAX_VALUE
/**
* 在被挤下线时 ([BotOfflineEvent.Force]) 自动重连. 默认为 `false`.
*
* 其他情况掉线都默认会自动重连, 详见 [BotOfflineEvent.reconnect]
*
* @since 2.1
*/
public actual var autoReconnectOnForceOffline: Boolean = false
/**
* 验证码处理器
*
* - Android 需要手动提供 [LoginSolver]
* - JVM, Mirai 会根据环境支持情况选择 Swing/CLI 实现
*
* 详见 [LoginSolver.Default]
*
* @see LoginSolver
*/
public actual var loginSolver: LoginSolver? = LoginSolver.Default
/** 使用协议类型 */
public actual var protocol: MiraiProtocol = MiraiProtocol.ANDROID_PHONE
public actual enum class MiraiProtocol {
/**
* Android 手机. 所有功能都支持.
*/
ANDROID_PHONE,
/**
* Android 平板.
*
* 注意: 不支持戳一戳事件解析
*/
ANDROID_PAD,
/**
* Android 手表.
*/
ANDROID_WATCH,
/**
* iPad - 来自MiraiGo
*
* @since 2.8
*/
IPAD,
/**
* MacOS - 来自MiraiGo
*
* @since 2.8
*/
MACOS,
}
/**
* Highway 通道上传图片, 语音, 文件等资源时的协程数量.
*
* 每个协程的速度约为 200KB/s. 协程数量越多越快, 同时也更要求性能.
* 默认: CPU 核心数.
*
* @since 2.2
*/
public actual var highwayUploadCoroutineCount: Int = availableProcessors()
/**
* 设置 [autoReconnectOnForceOffline] `true`, 即在被挤下线时自动重连.
* @since 2.1
*/
@ConfigurationDsl
public actual fun autoReconnectOnForceOffline() {
autoReconnectOnForceOffline = true
}
///////////////////////////////////////////////////////////////////////////
// Device
///////////////////////////////////////////////////////////////////////////
internal actual var accountSecrets: Boolean = true
/**
* 禁止保存 `account.secrets`.
*
* `account.secrets` 保存账号的会话信息
* 它可加速登录过程也可能可以减少出现验证码的次数如果遇到一段时间后无法接收消息通知等同步问题时可尝试禁用
*
* @since 2.11
*/
public actual fun disableAccountSecretes() {
accountSecrets = false
}
/**
* 设备信息覆盖. 在没有手动指定时将会通过日志警告, 并使用随机设备信息.
* @see fileBasedDeviceInfo 使用指定文件存储设备信息
* @see randomDeviceInfo 使用随机设备信息
*/
public actual var deviceInfo: ((Bot) -> DeviceInfo)? = deviceInfoStub // allows user to set `null` manually.
/**
* 使用随机设备信息.
*
* @see deviceInfo
*/
@ConfigurationDsl
public actual fun randomDeviceInfo() {
deviceInfo = null
}
/**
* 使用特定由 [DeviceInfo] 序列化产生的 JSON 的设备信息
*
* @see deviceInfo
*/
@ConfigurationDsl
public actual fun loadDeviceInfoJson(json: String) {
deviceInfo = {
Companion.json.decodeFromString(DeviceInfo.serializer(), json)
}
}
/**
* 使用文件存储设备信息.
*
* 此函数只在 JVM Android 有效. 在其他平台将会抛出异常.
* @param filepath 文件路径. 默认是相对于 [workingDir] 的文件 "device.json".
* @see deviceInfo
*/
@ConfigurationDsl
public actual fun fileBasedDeviceInfo(filepath: String) {
deviceInfo = TODO("native")
}
///////////////////////////////////////////////////////////////////////////
// Logging
///////////////////////////////////////////////////////////////////////////
/**
* 日志记录器
*
* - 默认打印到标准输出, 通过 [MiraiLogger.create]
* - 忽略所有日志: [noBotLog]
* - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
* - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
*
* @see MiraiLogger
*/
public actual var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
}
/**
* 网络层日志构造器
*
* - 默认打印到标准输出, 通过 [MiraiLogger.create]
* - 忽略所有日志: [noNetworkLog]
* - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
* - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
*
* @see MiraiLogger
*/
public actual var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
}
/**
* 不显示网络日志. 不推荐.
* @see networkLoggerSupplier 更多日志处理方式
*/
@ConfigurationDsl
public actual fun noNetworkLog() {
networkLoggerSupplier = { _ -> SilentLogger }
}
/**
* 不显示 [Bot] 日志. 不推荐.
* @see botLoggerSupplier 更多日志处理方式
*/
@ConfigurationDsl
public actual fun noBotLog() {
botLoggerSupplier = { _ -> SilentLogger }
}
/**
* 是否显示过于冗长的事件日志
*
* 默认为 `false`
*
* @since 2.8
*/
public actual var isShowingVerboseEventLog: Boolean = false
///////////////////////////////////////////////////////////////////////////
// Cache
//////////////////////////////////////////////////////////////////////////
/**
* 缓存数据目录, 相对于 [workingDir].
*
* 缓存目录保存的内容均属于不稳定的 Mirai 内部数据, 请不要手动修改它们. 清空缓存不会影响功能. 只会导致一些操作如读取全部群列表要重新进行.
* 默认启用的缓存可以加快登录过程.
*
* 注意: 这个目录只存储能在 [BotConfiguration] 配置的内容, 即包含:
* - 联系人列表
* - 登录服务器列表
* - 资源服务秘钥
*
* 其他内容如通过 [Input] 发送图片时的缓存使用 [FileCacheStrategy], 默认使用系统临时文件且会在关闭时删除文件.
*
* @since 2.4
*/
public var cacheDir: String = workingDir + "/cache"
/**
* 联系人信息缓存配置. 将会保存在 [cacheDir] `contacts` 目录
* @since 2.4
*/
public actual var contactListCache: ContactListCache = ContactListCache()
/**
* 联系人信息缓存配置
* @see contactListCache
* @see enableContactCache
* @see disableContactCache
* @since 2.4
*/
public actual class ContactListCache {
/**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/
public actual var saveIntervalMillis: Long = 60_000
/**
* 在有修改时自动保存间隔. 默认 60 . 在每次登录完成后有修改时都会立即保存一次.
*/ // was @ExperimentalTime before 2.9
public actual inline var saveInterval: Duration
inline get() = saveIntervalMillis.milliseconds
inline set(v) {
saveIntervalMillis = v.inWholeMilliseconds
}
/**
* 开启好友列表缓存.
*/
public actual var friendListCacheEnabled: Boolean = false
/**
* 开启群成员列表缓存.
*/
public actual var groupMemberListCacheEnabled: Boolean = false
}
/**
* 配置 [ContactListCache]
* ```
* contactListCache {
* saveIntervalMillis = 30_000
* friendListCacheEnabled = true
* }
* ```
* @since 2.4
*/
public actual inline fun contactListCache(action: ContactListCache.() -> Unit) {
action.invoke(this.contactListCache)
}
/**
* 禁用好友列表和群成员列表的缓存.
* @since 2.4
*/
@ConfigurationDsl
public actual fun disableContactCache() {
contactListCache.friendListCacheEnabled = false
contactListCache.groupMemberListCacheEnabled = false
}
/**
* 启用好友列表和群成员列表的缓存.
* @since 2.4
*/
@ConfigurationDsl
public actual fun enableContactCache() {
contactListCache.friendListCacheEnabled = true
contactListCache.groupMemberListCacheEnabled = true
}
/**
* 登录缓存.
*
* 开始后在密码登录成功时会保存秘钥等信息, 在下次启动时通过这些信息登录, 而不提交密码.
* 可以减少验证码出现的频率.
*
* 秘钥信息会由密码加密保存. 如果秘钥过期, 则会进行普通密码登录.
*
* 默认 `true` (开启).
*
* @since 2.6
*/
public actual var loginCacheEnabled: Boolean = true
///////////////////////////////////////////////////////////////////////////
// Misc
///////////////////////////////////////////////////////////////////////////
@Suppress("DuplicatedCode")
public actual fun copy(): BotConfiguration {
return BotConfiguration().also { new ->
// To structural order
new.workingDir = workingDir
new.parentCoroutineContext = parentCoroutineContext
new.heartbeatPeriodMillis = heartbeatPeriodMillis
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
new.statHeartbeatPeriodMillis = statHeartbeatPeriodMillis
new.heartbeatStrategy = heartbeatStrategy
new.reconnectionRetryTimes = reconnectionRetryTimes
new.autoReconnectOnForceOffline = autoReconnectOnForceOffline
new.loginSolver = loginSolver
new.protocol = protocol
new.highwayUploadCoroutineCount = highwayUploadCoroutineCount
new.accountSecrets = accountSecrets
new.deviceInfo = deviceInfo
new.botLoggerSupplier = botLoggerSupplier
new.networkLoggerSupplier = networkLoggerSupplier
new.cacheDir = cacheDir
new.contactListCache = contactListCache
new.convertLineSeparator = convertLineSeparator
new.isShowingVerboseEventLog = isShowingVerboseEventLog
}
}
/**
* 是否处理接受到的特殊换行符, 默认为 `true`
*
* - 若为 `true`, 会将收到的 `CRLF(\r\n)` `CR(\r)` 替换为 `LF(\n)`
* - 若为 `false`, 则不做处理
*
* @since 2.4
*/
public actual var convertLineSeparator: Boolean = true
/** 标注一个配置 DSL 函数 */
@Target(AnnotationTarget.FUNCTION)
@DslMarker
public actual annotation class ConfigurationDsl
public actual companion object {
/** 默认的配置实例. 可以进行修改 */
public actual val Default: BotConfiguration = BotConfiguration()
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = true
}
}
}

View File

@ -0,0 +1,121 @@
/*
* 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 kotlinx.io.core.toByteArray
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlin.random.Random
@Serializable
public actual class DeviceInfo actual constructor(
public actual val display: ByteArray,
public actual val product: ByteArray,
public actual val device: ByteArray,
public actual val board: ByteArray,
public actual val brand: ByteArray,
public actual val model: ByteArray,
public actual val bootloader: ByteArray,
public actual val fingerprint: ByteArray,
public actual val bootId: ByteArray,
public actual val procVersion: ByteArray,
public actual val baseBand: ByteArray,
public actual val version: Version,
public actual val simInfo: ByteArray,
public actual val osType: ByteArray,
public actual val macAddress: ByteArray,
public actual val wifiBSSID: ByteArray,
public actual val wifiSSID: ByteArray,
public actual val imsiMd5: ByteArray,
public actual val imei: String,
public actual val apn: ByteArray
) {
public actual val androidId: ByteArray get() = display
public actual val ipAddress: ByteArray get() = byteArrayOf(192.toByte(), 168.toByte(), 1, 123)
init {
require(imsiMd5.size == 16) { "Bad `imsiMd5.size`. Required 16, given ${imsiMd5.size}." }
}
@Transient
@MiraiInternalApi
public actual val guid: ByteArray = generateGuid(androidId, macAddress)
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // serializable
@Serializable
public actual class Version actual constructor(
public actual val incremental: ByteArray = "5891938".toByteArray(),
public actual val release: ByteArray = "10".toByteArray(),
public actual val codename: ByteArray = "REL".toByteArray(),
public actual val sdk: Int = 29
) {
/**
* @since 2.9
*/
actual override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Version) return false
if (!incremental.contentEquals(other.incremental)) return false
if (!release.contentEquals(other.release)) return false
if (!codename.contentEquals(other.codename)) return false
if (sdk != other.sdk) return false
return true
}
/**
* @since 2.9
*/
actual override fun hashCode(): Int {
var result = incremental.contentHashCode()
result = 31 * result + release.contentHashCode()
result = 31 * result + codename.contentHashCode()
result = 31 * result + sdk
return result
}
}
public actual companion object {
internal actual val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
/**
* 生成随机 [DeviceInfo]
*
* @since 2.0
*/
public actual fun random(): DeviceInfo = random(Random.Default)
/**
* 使用特定随机数生成器生成 [DeviceInfo]
*
* @since 2.9
*/
public actual fun random(random: Random): DeviceInfo {
return DeviceInfoCommonImpl.randomDeviceInfo(random)
}
}
/**
* @since 2.9
*/
@Suppress("DuplicatedCode")
actual override fun equals(other: Any?): Boolean {
return DeviceInfoCommonImpl.equalsImpl(this, other)
}
/**
* @since 2.9
*/
actual override fun hashCode(): Int {
return DeviceInfoCommonImpl.hashCodeImpl(this)
}
}

View File

@ -0,0 +1,273 @@
/*
* 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 kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.io.core.Input
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.internal.utils.*
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.Image
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage
/**
* 一个*不可变的*外部资源. 仅包含资源内容, 大小, 文件类型, 校验值而不包含文件名, 文件位置等. 外部资源有可能是一个文件, 也有可能只存在于内存, 或者以任意其他方式实现.
*
* [ExternalResource] 在创建之后就应该保持其属性的不变, 即任何时候获取其属性都应该得到相同结果, 任何时候打开流都得到的一样的数据.
*
* # 创建
* - [ByteArray.toExternalResource]
*
* ## Kotlin 获得和使用 [ExternalResource] 实例
*
* ```
* file.toExternalResource().use { resource -> // 安全地使用资源
* contact.uploadImage(resource) // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
* }
* ```
*
* 注意, 若使用 [Input], 必须手动关闭 [Input]. 一种使用情况示例:
*
* ```
* inputStream.use { input -> // 安全地使用 InputStream
* input.toExternalResource().use { resource -> // 安全地使用资源
* contact.uploadImage(resource) // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file) // 或者用来上传文件
* }
* }
* ```
*
* ## Java 获得和使用 [ExternalResource] 实例
*
* ```
* try (ExternalResource resource = ExternalResource.create(file)) { // 使用文件 file
* contact.uploadImage(resource); // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
* }
* ```
*
* 注意, 若使用 [Input], 必须手动关闭 [Input]. 一种使用情况示例:
*
* ```java
* try (InputStream stream = ...) { // 安全地使用 InputStream
* try (ExternalResource resource = ExternalResource.create(stream)) { // 安全地使用资源
* contact.uploadImage(resource); // 用来上传图片
* contact.files.uploadNewFile("/foo/test.txt", file); // 或者用来上传文件
* }
* }
* ```
*
* # 释放
*
* [ExternalResource] 创建时就可能会打开一些资源.
* 类似于 [Input], [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
*/
@MiraiExperimentalApi
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>
/**
* 打开 [Input]. 在返回的 [Input] [关闭][Input.close] 前无法再次打开流.
*
* 关闭此流不会关闭 [ExternalResource].
* @throws IllegalStateException 当上一个流未关闭又尝试打开新的流时抛出
*
* @since SINCE_NATIVE_TARGET
*/
public actual fun input(): Input
@MiraiInternalApi
public actual fun calculateResourceId(): String {
return generateImageId(md5, formatName.ifEmpty { DEFAULT_FORMAT_NAME })
}
/**
* [ExternalResource] 的数据来源, 可能有以下的返回
*
* - [ByteArray] RAM
* - ...
*
* implementation note:
*
* - 对于无法二次读取的数据来源 ( [Input]), 返回 `null`
* - 不要返回 [String], 没有约定 [String] 代表什么
* - 数据源外漏会严重影响 [inputStream] 等的执行的可以返回 `null` (如文件句柄)
*
* @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] 关闭.
*
* @param formatName 查看 [ExternalResource.formatName]
*/
public actual fun ByteArray.toExternalResource(formatName: String?): ExternalResource =
ExternalResourceImplByByteArray(this, formatName)
// endregion
///////////////////////////////////////////////////////////////////////////
// region sendAsImageTo
///////////////////////////////////////////////////////////////////////////
/**
* 将图片作为单独的消息发送给指定联系人.
*
* **注意**本函数不会关闭 [ExternalResource].
*
* @see Contact.uploadImage 上传图片
* @see Contact.sendMessage 最终调用, 发送消息.
*
* @throws OverFileSizeMaxException
*/
public actual suspend fun <C : Contact> ExternalResource.sendAsImageTo(contact: C): MessageReceipt<C> =
contact.uploadImage(this).sendTo(contact)
// endregion
///////////////////////////////////////////////////////////////////////////
// region uploadAsImage
///////////////////////////////////////////////////////////////////////////
/**
* 上传图片并构造 [Image]. 这个函数可能需消耗一段时间.
*
* **注意**本函数不会关闭 [ExternalResource].
*
* @param contact 图片上传对象. 由于好友图片与群图片不通用, 上传时必须提供目标联系人.
*
* @see Contact.uploadImage 最终调用, 上传图片.
*/
public actual suspend fun ExternalResource.uploadAsImage(contact: Contact): Image = contact.uploadImage(this)
// endregion
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 net.mamoe.mirai.Bot
import net.mamoe.mirai.network.LoginFailedException
import net.mamoe.mirai.utils.LoginSolver.Companion.Default
/**
* 验证码, 设备锁解决器
*
* @see Default
* @see BotConfiguration.loginSolver
*/
public actual abstract class LoginSolver actual constructor() {
/**
* 处理图片验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolvePicCaptcha(bot: Bot, data: ByteArray): String?
/**
* `true` 表示支持滑动验证码, 遇到滑动验证码时 mirai 会请求 [onSolveSliderCaptcha].
* 否则会跳过滑动验证码并告诉服务器此客户端不支持, 有可能导致登录失败
*/
public actual open val isSliderCaptchaSupported: Boolean
get() = false
/**
* 处理滑动验证码.
*
* 返回 `null` 以表示无法处理验证码, 将会刷新验证码或重试登录.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止
*
* @throws LoginFailedException
* @return 验证码解决成功后获得的 ticket.
*/
public actual abstract suspend fun onSolveSliderCaptcha(bot: Bot, url: String): String?
/**
* 处理不安全设备验证.
*
* 返回值保留给将来使用. 目前在处理完成后返回任意内容 (包含 `null`) 均视为处理成功.
* 抛出一个 [LoginFailedException] 以正常地终止登录, 抛出任意其他 [Exception] 将视为异常终止.
*
* @return 任意内容. 返回值保留以供未来更新.
* @throws LoginFailedException
*/
public actual abstract suspend fun onSolveUnsafeDeviceLoginVerify(
bot: Bot,
url: String
): String?
public actual companion object {
/**
* 当前平台默认的 [LoginSolver]
*
* 检测策略:
* 1. 若是 `mirai-core-api-android` `android.util.Log` 存在, 返回 `null`.
* 2. 检测 JVM 属性 `mirai.no-desktop`. 若存在, 返回 `StandardCharImageLoginSolver`
* 3. 检测 JVM 桌面环境, 若支持, 返回 `SwingSolver`
* 4. 返回 `StandardCharImageLoginSolver`
*
* @return `SwingSolver` `StandardCharImageLoginSolver` `null`
*/
public actual val Default: LoginSolver?
get() = TODO("Not yet implemented")
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
@Suppress("unused")
public actual fun getDefault(): LoginSolver = Default
?: error("LoginSolver is not provided by default on your platform. Please specify by BotConfiguration.loginSolver")
}
}

View File

@ -0,0 +1,254 @@
/*
* 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 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.setDefaultLoggerCreator] 全局覆盖日志.
*
* ## 实现或使用 [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 = TODO("native")
/**
* 创建 [MiraiLogger] 实例.
*
* @param requester 请求创建 [MiraiLogger] 的对象
*/
public actual fun create(requester: KClass<*>): MiraiLogger = TODO("native")
public actual companion object INSTANCE : Factory by TODO("native")
}
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 actual val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
/**
* 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated(
"Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory",
level = DeprecationLevel.ERROR
) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally, for internal uses.
public actual fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
throw UnsupportedOperationException()
}
/**
* 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
*
* @see setDefaultLoggerCreator
*/
@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
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public actual 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
/**
* 随从. this 中调用所有方法后都应继续往 [follower] 传递调用.
* [follower] 的存在可以让一次日志被多个日志记录器记录.
*
* 一般不建议直接修改这个属性. 请通过 [plus] 来连接两个日志记录器.
* : `val logger = bot.logger + MyLogger()`
* 当调用 `logger.info()` , `bot.logger` 会首先记录, `MyLogger` 会随后记录.
*
* 当然, 多个 logger 也可以加在一起: `val logger = bot.logger + MynLogger() + MyLogger2()`
*/
@Suppress("UNUSED_PARAMETER")
@Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public actual 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 =
priority.correspondingFunction(this, message, e)
/**
* 添加一个 [follower], 返回 [follower]
* 它只会把 `this` 的属性 [MiraiLogger.follower] 修改为这个函数的参数 [follower], 然后返回这个参数.
* [MiraiLogger.follower] 已经有值, 则会替换掉这个值.
* ```
* +------+ +----------+ +----------+ +----------+
* | base | <-- | follower | <-- | follower | <-- | follower |
* +------+ +----------+ +----------+ +----------+
* ```
*
* @return [follower]
*/
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.HIDDEN) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10", hiddenSince = "2.11")
public actual operator fun <T : MiraiLogger> plus(follower: T): T = follower
}

View File

@ -0,0 +1,60 @@
/*
* 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
/**
* 当前平台的默认的日志记录器.
* - _JVM 控制台_ 端的实现为 [println]
* - _Android_ 端的实现为 `android.util.Log`
*
*
* 单条日志格式 (正则) :
* ```regex
* ^([\w-]*\s[\w:]*)\s(\w)\/(.*?):\s(.+)$
* ```
* 其中 group 分别为: 日期与时间, 严重程度, [identity], 消息内容.
*
* 示例:
* ```log
* 2020-05-21 19:51:09 V/Bot 123456789: Send: OidbSvc.0x88d_7
* ```
*
* 日期时间格式为 `yyyy-MM-dd HH:mm:ss`,
*
* 严重程度为 V, I, W, E. 分别对应 verbose, info, warning, error
*
* @see MiraiLogger.create
*/
@MiraiInternalApi
public actual open class PlatformLogger actual constructor(identity: String?) :
MiraiLoggerPlatformBase() {
override val identity: String?
get() = TODO("Not yet implemented")
override fun verbose0(message: String?, e: Throwable?) {
TODO("Not yet implemented")
}
override fun debug0(message: String?, e: Throwable?) {
TODO("Not yet implemented")
}
override fun info0(message: String?, e: Throwable?) {
TODO("Not yet implemented")
}
override fun warning0(message: String?, e: Throwable?) {
TODO("Not yet implemented")
}
override fun error0(message: String?, e: Throwable?) {
TODO("Not yet implemented")
}
}

View File

@ -0,0 +1,575 @@
/*
* 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:Suppress("unused", "DEPRECATION")
package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.toList
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.message.data.sendTo
import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile
import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
/**
* 表示一个远程文件或目录.
*
* [RemoteFile] 仅保存 [id], [name], [path], [parent], [contact] 这五个属性, 除获取这些属性外的所有的操作都是在*远程*完成的.
* 意味着操作的结果会因文件或目录在服务器中的状态变化而变化.
*
* [File] 类似, [RemoteFile] 是不可变的. [renameTo] [copyTo] 会操作远程文件, 但不会修改当前 [RemoteFile.path] 等属性.
*
* ## 文件操作
*
* 所有文件操作都在 [RemoteFile] 对象中完成. 可通过 [FileSupported.filesRoot] 获取到表示根目录路径的 [RemoteFile], 并通过 [resolve] 获取到其内文件.
*
* 示例:
* ```
* val file1: RemoteFile = group.filesRoot.resolve("/foo.txt") // 获取表示群文件 "foo.txt" 的 RemoteFile 实例
* val file2: RemoteFile = group.filesRoot.resolve("/dir/foo.txt") // 获取表示群文件目录 "dir" 中的 "foo.txt" 的 RemoteFile 实例
*
*
* val downloadInfo = file1.getDownloadInfo() // 获取该文件的下载方式, 可以自行下载
*
*
* val message: FileMessage = file2.upload(resource) // 向路径 "/dir/foo.txt" 上传一个文件, 返回可以发送到群内的文件消息.
* group.sendMessage(message) // 发送文件消息到群, 用户才会收到机器人上传文件的提醒. 可以多次发送.
*
* file2.uploadAndSend(resource) // 上传文件并发送文件消息. 是上面两行的简单版本.
*
*
* // 要直接上传文件, 也可以简单地使用任一:
* group.uploadFile("/foo.txt", resource) // Kotlin
* resource.uploadAsFileTo(group, "/foo.txt") // Kotlin
* FileSupported.uploadFile(group, "/foo.txt", resource"); // Java
* ExternalResource.uploadAsFile(resource, group, "/foo.txt") // Java
* ```
*
* ## 目录操作
* [RemoteFile] 类似于 [java.io.File], 也可以表示一个目录.
* ```
* val dir: RemoteFile = group.filesRoot.resolve("/foo") // 获取表示目录 "foo" 的 RemoteFile 实例
*
* if (dir.exists()) { // 判断目录是否存在
* // ...
* }
*
* dir.listFiles() // Kotlin 使用, 获取该目录中的文件列表.
* dir.listFilesIterator() // Java 使用, 获取该目录中的文件列表.
* ```
*
* 注意, 服务器目前只支持一层目录. 即只能存在 "/foo.txt" "/xxx/foo.txt", "/xxx/xxx/foo.txt" 不受支持.
*
* ## 文件名和目录名可重复
*
* 服务器允许相同名称的文件或目录存在, 这就导致 "/foo" 可能表示多个重名文件中的一个, 也可能表示一个目录. 依靠路径的判断因此不可靠.
*
* 这个特性带来的行为有:
* - [`FileSupported.uploadFile`][uploadFile] 总是往一个路径上传文件, 如果有同名文件存在, 不会覆盖, 而是再创建一个同名文件.
* - [delete] 可能会删除重名文件中的任何一个, 也可能会删除一个目录, 操作顺序取决于服务器.
*
* 为了解决这个问题, [RemoteFile] 可以拥有一个由服务器分配的固定的唯一识别号 [RemoteFile.id].
*
* 通过 [listFiles] 获取到的 [RemoteFile] 都拥有非 `null` [id].
* 服务器可以通过 [id] 准确定位重名文件中的某一个.
* 对这样的文件进行 [upload] 时将会覆盖目标文件 (如果存在), 进行 [delete] 时也只会准确操作目标文件.
*
* 只要文件内容无变化, 文件的 [id] 就不会变更. 可以保存 [RemoteFile.id] 并在以后通过 [RemoteFile.resolveById] 准确获取一个目标文件.
*
* @suppress 使用 [RemoteFile] 是稳定的, 但不应该自行实现这个接口.
* @see FileSupported
* @since 2.5
*/
@Deprecated(
"Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files",
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
@NotStableForInheritance
public actual interface RemoteFile {
/**
* 文件名或目录名.
*/
public actual val name: String
/**
* 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
*/
public actual val id: String?
/**
* 标准的绝对路径, 起始字符为 '/'. `/foo/bar.txt`.
*
* 根目录路径为 [ROOT_PATH]
*/
public actual val path: String
/**
* 获取父目录, [RemoteFile] 表示根目录时返回 `null`
*/
public actual val parent: RemoteFile?
/**
* 此文件所属的群或好友
*/
public actual val contact: FileSupported
/**
* [RemoteFile] 表示一个文件时返回 `true`.
*/
public actual suspend fun isFile(): Boolean
/**
* [RemoteFile] 表示一个目录时返回 `true`.
*/
public actual suspend fun isDirectory(): Boolean = !isFile()
/**
* 获取文件长度. [RemoteFile] 表示一个目录时行为不确定.
*/
public actual suspend fun length(): Long
public actual class FileInfo @MiraiInternalApi actual constructor(
/**
* 文件或目录名.
*/
public actual val name: String,
/**
* 唯一识别标识.
*/
public actual val id: String,
/**
* 标准绝对路径.
*/
public actual val path: String,
/**
* 文件长度 (大小) bytes, 目录的 [length] 0.
*/
public actual val length: Long,
/**
* 下载次数. 目录没有下载次数, 此属性总是 `0`.
*/
public actual val downloadTimes: Int,
/**
* 上传者 ID. 目录没有上传者, 此属性总是 `0`.
*/
public actual val uploaderId: Long,
/**
* 上传的时间. 目录没有上传时间, 此属性总是 `0`.
*/
public actual val uploadTime: Long,
/**
* 上次修改时间. 时间戳秒.
*/
public actual val lastModifyTime: Long,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
) {
/**
* 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile].
*/
public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
}
/**
* 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
*/
public actual suspend fun getInfo(): FileInfo?
/**
* 当文件或目录存在时返回 `true`.
*/
public actual suspend fun exists(): Boolean
/**
* @return [path]
*/
public actual override fun toString(): String
///////////////////////////////////////////////////////////////////////////
// resolve
///////////////////////////////////////////////////////////////////////////
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: String): RemoteFile
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
*
* @param relative 相对路径. [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: RemoteFile): RemoteFile
/**
* 获取该目录下的 ID [id] 的文件, [deep] `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
* @see resolve
*/
public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
/**
* 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null`
* @see resolve
*/
public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
* 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: String): RemoteFile
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
* 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
*
* @param relative [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: RemoteFile): RemoteFile
///////////////////////////////////////////////////////////////////////////
// operations
///////////////////////////////////////////////////////////////////////////
/**
* 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*/
public actual suspend fun delete(): Boolean
/**
* 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
* 操作非 Bot 自己上传的文件时需要管理员权限.
*
* [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*/
public actual suspend fun renameTo(name: String): Boolean
/**
* 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*
* [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*
* **注意**: [java.io.File] 类似, 这是将当前 [RemoteFile] 移动到作为 [target], 而不是移动成为 [target] 的子文件或目录. 例如:
* ```
* val root = group.filesRoot
* root.resolve("test.txt").moveTo(root) // 错误! 这是在将该文件的路径 "test.txt" 修改为 “/” , 而不是修改为 "/test.txt"
* root.resolve("test.txt").moveTo(root.resolve("/")) // 错误! 与上一行相同.
* root.resolve("/test.txt").moveTo(root.resolve("/test2.txt")) // 正确. 将该文件的路径 "/test.txt" 修改为 “/test2.txt”相当于重命名文件
* ```
*
* @param target 目标文件位置.
*/
public actual suspend fun moveTo(target: RemoteFile): Boolean
/**
* 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*
* [moveTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*
* **已弃用:** [path] 是绝对路径时, 这个函数运行正常;
* 当它是相对路径时, 将会尝试把当前文件移动到 [RemoteFile.path] 下的子路径 [path], 因此总是失败.
*
* 使用参数为 [RemoteFile] [moveTo] 代替.
*
* @suppress 2.6 弃用. 请使用 [moveTo]
*/
@Deprecated(
"Use moveTo(RemoteFile) instead.",
replaceWith = ReplaceWith("this.moveTo(this.resolveSibling(path))"),
level = DeprecationLevel.ERROR
) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
public actual suspend fun moveTo(path: String): Boolean {
// Impl notes:
// if `path` is absolute, this works as intended.
// if not, `resolve(path)` will be a child path from this dir and fails always.
return moveTo(resolve(path))
}
/**
* 创建目录. 目录已经存在或无管理员权限时返回 `false`.
*
* 创建后 [isDirectory] 也不一定会返回 `true`.
* [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] [isDirectory] 结果取决于服务器.
*/
public actual suspend fun mkdir(): Boolean
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyFlow].
*
* 返回的 [Flow] **, 只会在被需要的时候向服务器查询.
*/
public actual suspend fun listFiles(): Flow<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回空迭代器.
* @param lazy `true` 时惰性获取, `false` 时立即获取全部文件列表.
*/
@JavaFriendlyAPI
public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList].
*/
public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
/**
* 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
*/
public actual suspend fun toMessage(): FileMessage?
///////////////////////////////////////////////////////////////////////////
// upload & download
///////////////////////////////////////////////////////////////////////////
/**
* 上传进度回调, 可供前端使用, 以提供进度显示.
* @see asProgressionCallback
*/
@Deprecated(
"Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile",
ReplaceWith("contact.files.uploadNewFile(path, this, callback)"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual interface ProgressionCallback {
/**
* 当上传开始时调用
*/
public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
/**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
*
* 提示: 可通过 [ExternalResource.size] 获取文件总大小.
*/
public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
/**
* 当上传成功时调用
*/
public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
/**
* 当上传以异常失败时调用
*/
public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
public actual companion object {
/**
* 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
*
* 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer] [SendChannel] .
* 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存.
*
* [closeOnFinish] `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel].
*
* 使用示例:
* ```
* val progress = Channel<Long>(Channel.BUFFERED)
*
* launch {
* // 每 3 秒发送一次上传进度百分比
* progress.receiveAsFlow().sample(3.seconds).collect { bytes ->
* group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数
* }
* }
*
* group.filesRoot.resolve("/foo.txt").upload(resource, progress.asProgressionCallback(true))
* group.sendMessage("File uploaded successfully.")
* ```
*
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
*/
public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
return object : ProgressionCallback {
override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
trySend(downloadedSize)
}
override fun onSuccess(file: RemoteFile, resource: ExternalResource) {
if (closeOnFinish) this@asProgressionCallback.close()
}
override fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {
if (closeOnFinish) this@asProgressionCallback.close(exception)
}
}
}
}
}
/**
* 上传文件到 [RemoteFile] 表示的路径, 上传过程中调用 [callback] 传递进度.
*
* 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件.
* 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend].
*
* ## 已弃用
*
* 使用 [sendFile] 代替. 本函数会上传文件但不会发送文件消息.
* 不发送文件消息就导致其他操作都几乎不能完成, 而且经反馈, 用户通常会忘记后续的 [RemoteFile.toMessage] 操作.
* 本函数造成了很大的不必要的迷惑, 故以既上传又发送消息的, 与官方客户端行为相同的 [sendFile] 代替.
*
* 相关问题: [#1250: 群文件在上传后 toRemoteFile 返回 null](https://github.com/mamoe/mirai/issues/1250)
*
*
* **注意**: [resource] 仅表示资源数据, 而不带有文件名属性.
* [java.io.File] 类似, [upload] 是将 [resource] 上传成为 [this][RemoteFile], 而不是上传成为 [this][RemoteFile] 的子文件. 示例:
* ```
* group.filesRoot.upload(resource) // 错误! 这是在把资源上传成为根目录.
* group.filesRoot.resolve("/").upload(resource) // 错误! 与上一句相同, 这是在把资源上传成为根目录.
*
* val root = group.filesRoot
* root.resolve("test.txt").upload(resource) // 正确. 把资源上传成为根目录下的 "test.txt".
* root.resolve("/test.txt").upload(resource) // 正确. 与上一句相同, 把资源上传成为根目录下的 "test.txt".
* ```
*
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @param callback 进度回调
* @throws IllegalStateException 该文件上传失败或权限不足时抛出
*/
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun upload(
resource: ExternalResource,
callback: ProgressionCallback?,
): FileMessage
/**
* 上传文件到 [RemoteFile.path] 表示的路径.
* ## 已弃用
* 阅读 [upload] 获取更多信息
* @see upload
*/
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
/**
* 上传文件并发送文件消息.
*
* [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件.
* 即使用 [resolve] [resolveSibling] 获取到的 [RemoteFile] [upload] 总是上传一个新文件,
* 而使用 [resolveById] [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件.
*
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see upload
*/
@MiraiExperimentalApi
public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
/**
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
*/
public actual suspend fun getDownloadInfo(): DownloadInfo?
public actual class DownloadInfo @MiraiInternalApi actual constructor(
/**
* @see RemoteFile.name
*/
public actual val filename: String,
/**
* @see RemoteFile.id
*/
public actual val id: String,
/**
* 标准绝对路径
* @see RemoteFile.path
*/
public actual val path: String,
/**
* HTTP or HTTPS URL
*/
public actual val url: String,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
) {
actual override fun toString(): String {
return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
"md5=${md5.toUHexString("")})"
}
}
public actual companion object {
/**
* 根目录路径
* @see RemoteFile.path
*/
public actual const val ROOT_PATH: String = "/"
/**
* 上传文件并获取文件消息, 但不发送.
*
* ## 已弃用
* [upload] 获取更多信息
*
* @param path 远程路径. 起始字符为 '/'. '/foo/bar.txt'
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see RemoteFile.upload
*/
@Deprecated(
"Use sendFile instead.",
ReplaceWith(
"this.sendFile(path, resource, callback)",
"net.mamoe.mirai.utils.RemoteFile.Companion.sendFile"
),
level = DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun FileSupported.uploadFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
): FileMessage =
@Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
/**
* 上传文件并发送文件消息到相关 [FileSupported].
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see RemoteFile.uploadAndSend
*/
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual suspend fun <C : FileSupported> C.sendFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
): MessageReceipt<C> =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
}
}

View File

@ -12,6 +12,9 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
public inline fun <A, reified B> Array<A>.mapToArray(block: (element: A) -> B): Array<B> { public inline fun <A, reified B> Array<A>.mapToArray(block: (element: A) -> B): Array<B> {
val result = arrayOfNulls<B>(size) val result = arrayOfNulls<B>(size)
this.forEachIndexed { index, element -> this.forEachIndexed { index, element ->

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -7,9 +7,6 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass
@file:JvmName("MiraiUtils")
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.io.pool.DefaultPool import kotlinx.io.pool.DefaultPool

View File

@ -16,6 +16,10 @@ package net.mamoe.mirai.utils
import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.ByteReadPacket
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmSynthetic
@JvmOverloads @JvmOverloads

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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.utils
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.mamoe.mirai.utils.Either.Companion.fold import net.mamoe.mirai.utils.Either.Companion.fold
import kotlin.jvm.JvmName
import kotlin.reflect.KType import kotlin.reflect.KType
@Suppress("PropertyName") @Suppress("PropertyName")

View File

@ -0,0 +1,17 @@
/*
* 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 kotlinx.io.errors.IOException
public expect interface Closeable {
@Throws(IOException::class)
public fun close()
}

View File

@ -0,0 +1,13 @@
/*
* 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
@Suppress("FunctionName")
public expect fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V>

View File

@ -1,15 +1,17 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
public fun <T : Any> computeOnNullMutableProperty(initializer: () -> T): ComputeOnNullMutableProperty<T> = public fun <T : Any> computeOnNullMutableProperty(initializer: () -> T): ComputeOnNullMutableProperty<T> =
@ -28,10 +30,11 @@ private class ComputeOnNullMutablePropertyImpl<T : Any>(
private val initializer: () -> T private val initializer: () -> T
) : ComputeOnNullMutableProperty<T> { ) : ComputeOnNullMutableProperty<T> {
private val value = atomic<T?>(null) private val value = atomic<T?>(null)
private val lock = SynchronizedObject()
override tailrec fun get(): T { override tailrec fun get(): T {
return when (val v = this.value.value) { return when (val v = this.value.value) {
null -> synchronized(this) { null -> synchronized(lock) {
if (this.value.value === null) { if (this.value.value === null) {
val value = this.initializer() val value = this.initializer()
// compiler inserts // compiler inserts

View File

@ -14,6 +14,9 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/* /*
* 类型转换 Utils. * 类型转换 Utils.
* 这些函数为内部函数, 可能会改变 * 这些函数为内部函数, 可能会改变

View File

@ -7,8 +7,8 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass
@file:JvmName("MiraiUtils") @file:JvmName("CoroutineUtils_common")
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
@ -17,25 +17,15 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
@Suppress("unused", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "DeprecatedCallableAddReplaceWith") public expect suspend inline fun <R> runBIO(
@Deprecated(
message = "Use runBIO which delegates to `runInterruptible`. " +
"Technically remove suspend call in `block` and remove CoroutineScope parameter usages.",
level = DeprecationLevel.HIDDEN
)
@kotlin.internal.LowPriorityInOverloadResolution
public suspend inline fun <R> runBIO(
noinline block: suspend CoroutineScope.() -> R,
): R = withContext(Dispatchers.IO, block)
public suspend inline fun <R> runBIO(
noinline block: () -> R, noinline block: () -> R,
): R = runInterruptible(context = Dispatchers.IO, block = block) ): R
public suspend inline fun <T, R> T.runBIO( public expect suspend inline fun <T, R> T.runBIO(
crossinline block: T.() -> R, crossinline block: T.() -> R,
): R = runInterruptible(context = Dispatchers.IO, block = { block() }) ): R
public inline fun CoroutineScope.launchWithPermit( public inline fun CoroutineScope.launchWithPermit(
semaphore: Semaphore, semaphore: Semaphore,

View File

@ -11,6 +11,10 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmField
import kotlin.jvm.JvmInline
import kotlin.jvm.JvmName
/** /**
* Safe union of two types. * Safe union of two types.
*/ */

View File

@ -11,6 +11,8 @@ package net.mamoe.mirai.utils
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.Synchronized
import kotlin.jvm.Volatile
public open class ExceptionCollector { public open class ExceptionCollector {
@ -50,16 +52,6 @@ public open class ExceptionCollector {
receiver.addSuppressed(e) receiver.addSuppressed(e)
} }
private fun hash(e: Throwable): Long {
return e.stackTrace.fold(0L) { acc, stackTraceElement ->
acc * 31 + hash(stackTraceElement).toLongUnsigned()
}
}
private fun hash(element: StackTraceElement): Int {
return element.lineNumber.hashCode() xor element.className.hashCode() xor element.methodName.hashCode()
}
public fun collectGet(e: Throwable?): Throwable { public fun collectGet(e: Throwable?): Throwable {
this.collect(e) this.collect(e)
return getLast()!! return getLast()!!
@ -90,7 +82,8 @@ public open class ExceptionCollector {
@TestOnly // very slow @TestOnly // very slow
public fun asSequence(): Sequence<Throwable> { public fun asSequence(): Sequence<Throwable> {
fun Throwable.itr(): Iterator<Throwable> { fun Throwable.itr(): Iterator<Throwable> {
return (sequenceOf(this) + this.suppressed.asSequence().flatMap { it.itr().asSequence() }).iterator() return (sequenceOf(this) + this.suppressedExceptions.asSequence()
.flatMap { it.itr().asSequence() }).iterator()
} }
val last = getLast() ?: return emptySequence() val last = getLast() ?: return emptySequence()
@ -137,3 +130,5 @@ public inline fun <R> ExceptionCollector.withExceptionCollector(action: Exceptio
} }
} }
} }
internal expect fun hash(e: Throwable): Long

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass
@ -12,6 +12,9 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
/** /**
* 文件头和文件类型列表 * 文件头和文件类型列表
*/ */

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass
@ -15,9 +15,11 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.io.charsets.Charset import kotlinx.io.charsets.Charset
import kotlinx.io.charsets.Charsets
import kotlinx.io.core.* import kotlinx.io.core.*
import java.io.File import kotlin.jvm.JvmMultifileClass
import kotlin.text.String import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
public val EMPTY_BYTE_ARRAY: ByteArray = ByteArray(0) public val EMPTY_BYTE_ARRAY: ByteArray = ByteArray(0)
@ -132,21 +134,3 @@ public inline fun Input.readString(length: Byte, charset: Charset = Charsets.UTF
public fun Input.readUShortLVString(): String = String(this.readUShortLVByteArray()) public fun Input.readUShortLVString(): String = String(this.readUShortLVByteArray())
public fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().toInt()) public fun Input.readUShortLVByteArray(): ByteArray = this.readBytes(this.readUShort().toInt())
public fun File.createFileIfNotExists() {
if (!this.exists()) {
this.parentFile.mkdirs()
this.createNewFile()
}
}
public fun File.resolveCreateFile(relative: String): File = this.resolve(relative).apply { createFileIfNotExists() }
public fun File.resolveCreateFile(relative: File): File = this.resolve(relative).apply { createFileIfNotExists() }
public fun File.resolveMkdir(relative: String): File = this.resolve(relative).apply { mkdirs() }
public fun File.resolveMkdir(relative: File): File = this.resolve(relative).apply { mkdirs() }
public fun File.touch(): File = apply {
parentFile?.mkdirs()
createNewFile()
}

View File

@ -1,15 +1,17 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.atomicfu.locks.SynchronizedObject
import kotlinx.atomicfu.locks.synchronized
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -24,7 +26,7 @@ public fun <T> lateinitMutableProperty(initializer: () -> T): ReadWriteProperty<
private class LateinitMutableProperty<T>( private class LateinitMutableProperty<T>(
initializer: () -> T initializer: () -> T
) : ReadWriteProperty<Any?, T> { ) : ReadWriteProperty<Any?, T>, SynchronizedObject() {
private val value = atomic(UNINITIALIZED) private val value = atomic(UNINITIALIZED)
private var initializer: (() -> T)? = initializer private var initializer: (() -> T)? = initializer
@ -39,7 +41,7 @@ private class LateinitMutableProperty<T>(
this.initializer = null this.initializer = null
this.value.compareAndSet(UNINITIALIZED, value) // setValue prevails this.value.compareAndSet(UNINITIALIZED, value) // setValue prevails
this.value.value.let { this.value.value.let {
assert(it !== UNINITIALIZED) check(it !== UNINITIALIZED)
return it as T return it as T
} }
} else this.value.value as T } else this.value.value as T

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass
@ -12,117 +12,39 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.io.core.Input import kotlinx.io.core.Closeable
import kotlinx.io.core.readAvailable import kotlinx.io.core.toByteArray
import java.io.* import kotlinx.io.core.use
import java.net.Inet4Address
import java.security.MessageDigest
import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.Inflater
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
@JvmOverloads public expect val DEFAULT_BUFFER_SIZE: Int
public fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray {
checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val inflater = Inflater() public expect fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteArray
inflater.reset()
ByteArrayOutputStream().use { output ->
inflater.setInput(this, offset, length)
ByteArray(DEFAULT_BUFFER_SIZE).let {
while (!inflater.finished()) {
output.write(it, 0, inflater.inflate(it))
}
}
inflater.end()
return output.toByteArray()
}
}
public fun InputStream.md5(): ByteArray {
return digest("md5")
}
public fun InputStream.digest(algorithm: String): ByteArray {
val digest = MessageDigest.getInstance(algorithm)
digest.reset()
use { input ->
object : OutputStream() {
override fun write(b: Int) {
digest.update(b.toByte())
}
override fun write(b: ByteArray, off: Int, len: Int) {
digest.update(b, off, len)
}
}.use { output ->
input.copyTo(output)
}
}
return digest.digest()
}
public fun InputStream.sha1(): ByteArray {
return digest("SHA-1")
}
/** /**
* Localhost 解析 * Localhost 解析
*/ */
public fun localIpAddress(): String = runCatching { public expect fun localIpAddress(): String
Inet4Address.getLocalHost().hostAddress
}.getOrElse { "192.168.1.123" }
public fun String.md5(): ByteArray = toByteArray().md5() public fun String.md5(): ByteArray = toByteArray().md5()
@JvmOverloads public expect fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray
public fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArray {
checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
}
public fun String.sha1(): ByteArray = toByteArray().sha1() public fun String.sha1(): ByteArray = toByteArray().sha1()
@JvmOverloads public expect fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray
public fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray {
checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest()
}
@JvmOverloads public expect fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray
public fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray {
return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() }
}
@JvmOverloads public expect fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray
public fun ByteArray.gzip(offset: Int = 0, length: Int = size - offset): ByteArray {
ByteArrayOutputStream().use { buf ->
GZIPOutputStream(buf).use { gzip ->
inputStream(offset, length).use { t -> t.copyTo(gzip) }
}
buf.flush()
return buf.toByteArray()
}
}
@JvmOverloads public expect fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray
public fun ByteArray.zip(offset: Int = 0, length: Int = size - offset): ByteArray {
checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val deflater = Deflater() public expect fun availableProcessors(): Int
deflater.setInput(this, offset, length)
deflater.finish()
ByteArray(DEFAULT_BUFFER_SIZE).let {
return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
}
}
public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R { public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R {
contract { contract {
@ -131,20 +53,6 @@ public inline fun <C : Closeable, R> C.withUse(block: C.() -> R): R {
return use(block) return use(block)
} }
@Throws(IOException::class)
@JvmOverloads
public fun Input.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = readAvailable(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = readAvailable(buffer)
}
return bytesCopied
}
public inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R { public inline fun <I : Closeable, O : Closeable, R> I.withOut(output: O, block: I.(output: O) -> R): R {
contract { contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE) callsInPlace(block, InvocationKind.EXACTLY_ONCE)

View File

@ -12,6 +12,9 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF) public fun Int.toLongUnsigned(): Long = this.toLong().and(0xFFFF_FFFF)
public fun Short.toIntUnsigned(): Int = this.toUShort().toInt() public fun Short.toIntUnsigned(): Int = this.toUShort().toInt()
public fun Byte.toIntUnsigned(): Int = toInt() and 0xFF public fun Byte.toIntUnsigned(): Int = toInt() and 0xFF

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass
@ -14,6 +14,8 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -9,13 +9,7 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import java.util.concurrent.atomic.AtomicInteger import kotlinx.atomicfu.atomic
@TestOnly
public fun readResource(url: String): String =
Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString()
?: error("Could not find resource '$url'")
public class ResourceAccessLock { public class ResourceAccessLock {
public companion object { public companion object {
@ -27,7 +21,7 @@ public class ResourceAccessLock {
/* /*
* status > 0 -> Number of holders using resource * status > 0 -> Number of holders using resource
*/ */
private val status = AtomicInteger(-1) private val status = atomic(-1)
/** /**
* ``` * ```
@ -54,7 +48,7 @@ public class ResourceAccessLock {
public fun tryUse(): Boolean { public fun tryUse(): Boolean {
val c = status val c = status
while (true) { while (true) {
val v = c.get() val v = c.value
if (v < 0) return false if (v < 0) return false
if (c.compareAndSet(v, v + 1)) return true if (c.compareAndSet(v, v + 1)) return true
} }
@ -63,7 +57,7 @@ public class ResourceAccessLock {
public fun lockIfNotUsing(): Boolean { public fun lockIfNotUsing(): Boolean {
val count = this.status val count = this.status
while (true) { while (true) {
val value = count.get() val value = count.value
if (value != 0) return false if (value != 0) return false
if (count.compareAndSet(0, -2)) return true if (count.compareAndSet(0, -2)) return true
} }
@ -72,7 +66,7 @@ public class ResourceAccessLock {
public fun release() { public fun release() {
val count = this.status val count = this.status
while (true) { while (true) {
val value = count.get() val value = count.value
if (value < 1) throw IllegalStateException("Current resource not in using") if (value < 1) throw IllegalStateException("Current resource not in using")
if (count.compareAndSet(value, value - 1)) return if (count.compareAndSet(value, value - 1)) return
@ -84,11 +78,11 @@ public class ResourceAccessLock {
} }
public fun setInitialized() { public fun setInitialized() {
status.set(INITIALIZED) status.value = INITIALIZED
} }
public fun setLocked() { public fun setLocked() {
status.set(LOCKED) status.value = LOCKED
} }
public fun setDisposed() { public fun setDisposed() {
@ -96,13 +90,13 @@ public class ResourceAccessLock {
} }
public fun setUninitialized() { public fun setUninitialized() {
status.set(UNINITIALIZED) status.value = UNINITIALIZED
} }
public fun currentStatus(): Int = status.get() public fun currentStatus(): Int = status.value
override fun toString(): String { override fun toString(): String {
return when (val status = status.get()) { return when (val status = status.value) {
0 -> "ResourceAccessLock(INITIALIZED)" 0 -> "ResourceAccessLock(INITIALIZED)"
-1 -> "ResourceAccessLock(UNINITIALIZED)" -1 -> "ResourceAccessLock(UNINITIALIZED)"
-2 -> "ResourceAccessLock(LOCKED)" -2 -> "ResourceAccessLock(LOCKED)"

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@ -13,6 +13,8 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.reflect.KClass import kotlin.reflect.KClass

View File

@ -7,41 +7,12 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmMultifileClass
@file:JvmName("MiraiUtils")
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.StringFormat
import kotlinx.serialization.descriptors.* import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import java.io.File
public fun <T> File.loadNotBlankAs(
serializer: DeserializationStrategy<T>,
stringFormat: StringFormat,
): T? {
if (!this.exists() || this.length() == 0L) {
return null
}
return stringFormat.decodeFromString(serializer, this.readText())
}
public fun <T> File.loadNotBlankAs(
serializer: DeserializationStrategy<T>,
binaryFormat: BinaryFormat,
): T? {
if (!this.exists() || this.length() == 0L) {
return null
}
return binaryFormat.decodeFromByteArray(serializer, this.readBytes())
}
public fun SerialDescriptor.copy(newName: String): SerialDescriptor = public fun SerialDescriptor.copy(newName: String): SerialDescriptor =
buildClassSerialDescriptor(newName) { takeElementsFrom(this@copy) } buildClassSerialDescriptor(newName) { takeElementsFrom(this@copy) }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -9,12 +9,13 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.atomicfu.locks.ReentrantLock
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock import kotlinx.atomicfu.locks.withLock
import java.util.concurrent.locks.ReentrantLock
@Suppress("unused", "UNCHECKED_CAST") @Suppress("unused", "UNCHECKED_CAST")
public class SizedCache<T>(size: Int) : Iterable<T> { public class SizedCache<T>(size: Int) : Iterable<T> {
public val lock: ReentrantLock = ReentrantLock() public val lock: ReentrantLock = reentrantLock()
public val data: Array<Any?> = arrayOfNulls(size) public val data: Array<Any?> = arrayOfNulls(size)
public var filled: Boolean = false public var filled: Boolean = false

View File

@ -7,14 +7,13 @@
* https://github.com/mamoe/mirai/blob/master/LICENSE * https://github.com/mamoe/mirai/blob/master/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmName("StandardUtilsKt_common")
@file:JvmName("MiraiUtils")
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import java.util.*
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.jvm.JvmName
public inline fun <reified T> Any?.cast(): T { public inline fun <reified T> Any?.cast(): T {
contract { returns() implies (this@cast is T) } contract { returns() implies (this@cast is T) }
@ -58,34 +57,6 @@ public inline fun <E> MutableList<E>.replaceAllKotlin(operator: (E) -> E) {
} }
} }
public fun <T> Collection<T>.asImmutable(): Collection<T> {
return when (this) {
is List<T> -> asImmutable()
is Set<T> -> asImmutable()
else -> Collections.unmodifiableCollection(this)
}
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> Collection<T>.asImmutableStrict(): Collection<T> {
return Collections.unmodifiableCollection(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> List<T>.asImmutable(): List<T> {
return Collections.unmodifiableList(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> Set<T>.asImmutable(): Set<T> {
return Collections.unmodifiableSet(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <K, V> Map<K, V>.asImmutable(): Map<K, V> {
return Collections.unmodifiableMap(this)
}
public fun Throwable.getRootCause(maxDepth: Int = 20): Throwable { public fun Throwable.getRootCause(maxDepth: Int = 20): Throwable {
var depth = 0 var depth = 0
var rootCause: Throwable? = this var rootCause: Throwable? = this
@ -156,7 +127,7 @@ public inline fun Throwable.findCauseOrSelf(maxDepth: Int = 20, filter: (Throwab
findCause(maxDepth, filter) ?: this findCause(maxDepth, filter) ?: this
public fun String.capitalize(): String { public fun String.capitalize(): String {
return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() } return replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
} }
public fun String.truncated(length: Int, truncated: String = "..."): String { public fun String.truncated(length: Int, truncated: String = "..."): String {
@ -178,3 +149,21 @@ public inline fun <T> T.context(block: T.() -> Unit) {
public fun assertUnreachable(hint: String? = null): Nothing = public fun assertUnreachable(hint: String? = null): Nothing =
error("This clause should not be reached. " + hint.orEmpty()) error("This clause should not be reached. " + hint.orEmpty())
public fun isSameClass(object1: Any?, object2: Any?): Boolean {
if (object1 == null || object2 == null) {
return object1 == null && object2 == null
}
return isSameClassPlatform(object1, object2)
}
internal expect fun isSameClassPlatform(object1: Any, object2: Any): Boolean
public inline fun <reified T> isSameType(thisObject: T, other: Any?): Boolean {
contract {
returns() implies (other is T)
}
if (other == null) return false
if (other !is T) return false
return isSameClass(thisObject, other)
}

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,16 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmName
public class Symbol private constructor(name: String) { public class Symbol private constructor(name: String) {
private val str = "Symbol($name)" private val str = "Symbol($name)"
override fun toString(): String = str override fun toString(): String = str

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass
@ -12,17 +12,15 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import kotlin.math.floor import kotlin.math.floor
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
/** public expect fun currentTimeMillis(): Long
* 时间戳
*
* @see System.currentTimeMillis
*/
public fun currentTimeMillis(): Long = System.currentTimeMillis()
/** /**
* 时间戳到秒 * 时间戳到秒

View File

@ -12,12 +12,12 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.concurrent.ConcurrentHashMap
import kotlin.contracts.InvocationKind import kotlin.contracts.InvocationKind
import kotlin.contracts.contract import kotlin.contracts.contract
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.jvm.JvmInline
@Serializable @Serializable
@JvmInline @JvmInline

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -9,6 +9,8 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import kotlin.jvm.JvmField
public fun <T : Any> unsafeMutableNonNullPropertyOf( public fun <T : Any> unsafeMutableNonNullPropertyOf(
name: String = "<unknown>" name: String = "<unknown>"
): UnsafeMutableNonNullProperty<T> { ): UnsafeMutableNonNullProperty<T> {
@ -24,7 +26,7 @@ public class UnsafeMutableNonNullProperty<T : Any>(
public val isInitialized: Boolean get() = value0 !== null public val isInitialized: Boolean get() = value0 !== null
public var value: T public var value: T
get() = value0 ?: throw UninitializedPropertyAccessException("Property `$propertyName` not initialized") get() = value0 ?: throw IllegalStateException("Property `$propertyName` not initialized")
set(value) { set(value) {
value0 = value value0 = value
} }

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@ -13,18 +13,20 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
import java.util.concurrent.ConcurrentHashMap import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
internal expect fun getProperty(name: String, default: String): String?
public fun systemProp(name: String, default: String): String = public fun systemProp(name: String, default: String): String =
System.getProperty(name, default) ?: default getProperty(name, default) ?: default
public fun systemProp(name: String, default: Boolean): Boolean = public fun systemProp(name: String, default: Boolean): Boolean =
System.getProperty(name, default.toString())?.toBoolean() ?: default getProperty(name, default.toString())?.toBoolean() ?: default
public fun systemProp(name: String, default: Long): Long = public fun systemProp(name: String, default: Long): Long =
System.getProperty(name, default.toString())?.toLongOrNull() ?: default getProperty(name, default.toString())?.toLongOrNull() ?: default
private val debugProps = ConcurrentHashMap<String, Boolean>() private val debugProps = ConcurrentHashMap<String, Boolean>()

View File

@ -0,0 +1,12 @@
/*
* 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
public actual typealias Closeable = java.io.Closeable

View File

@ -0,0 +1,46 @@
/*
* 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 java.util.*
public fun <T> Collection<T>.asImmutable(): Collection<T> {
return when (this) {
is List<T> -> asImmutable()
is Set<T> -> asImmutable()
else -> Collections.unmodifiableCollection(this)
}
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> Collection<T>.asImmutableStrict(): Collection<T> {
return Collections.unmodifiableCollection(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> List<T>.asImmutable(): List<T> {
return Collections.unmodifiableList(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <T> Set<T>.asImmutable(): Set<T> {
return Collections.unmodifiableSet(this)
}
@Suppress("NOTHING_TO_INLINE")
public inline fun <K, V> Map<K, V>.asImmutable(): Map<K, V> {
return Collections.unmodifiableMap(this)
}
@Suppress("FunctionName")
public actual fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V> {
return java.util.concurrent.ConcurrentHashMap()
}

View File

@ -0,0 +1,21 @@
/*
* 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
public actual suspend inline fun <R> runBIO(
noinline block: () -> R,
): R = runInterruptible(context = Dispatchers.IO, block = block)
public actual suspend inline fun <T, R> T.runBIO(
crossinline block: T.() -> R,
): R = runInterruptible(context = Dispatchers.IO, block = { block() })

View File

@ -0,0 +1,120 @@
/*
* 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("MiraiUtils")
package net.mamoe.mirai.utils
import kotlinx.io.core.use
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.Inet4Address
import java.security.MessageDigest
import java.util.zip.Deflater
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import java.util.zip.Inflater
public actual val DEFAULT_BUFFER_SIZE: Int get() = kotlin.io.DEFAULT_BUFFER_SIZE
public actual fun ByteArray.unzip(offset: Int, length: Int): ByteArray {
checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val inflater = Inflater()
inflater.reset()
ByteArrayOutputStream().use { output ->
inflater.setInput(this, offset, length)
ByteArray(DEFAULT_BUFFER_SIZE).let {
while (!inflater.finished()) {
output.write(it, 0, inflater.inflate(it))
}
}
inflater.end()
return output.toByteArray()
}
}
public actual fun localIpAddress(): String = runCatching {
Inet4Address.getLocalHost().hostAddress
}.getOrElse { "192.168.1.123" }
public fun InputStream.md5(): ByteArray {
return digest("md5")
}
public fun InputStream.digest(algorithm: String): ByteArray {
val digest = MessageDigest.getInstance(algorithm)
digest.reset()
use { input ->
object : OutputStream() {
override fun write(b: Int) {
digest.update(b.toByte())
}
override fun write(b: ByteArray, off: Int, len: Int) {
digest.update(b, off, len)
}
}.use { output ->
input.copyTo(output)
}
}
return digest.digest()
}
public fun InputStream.sha1(): ByteArray {
return digest("SHA-1")
}
public actual fun ByteArray.md5(offset: Int, length: Int): ByteArray {
checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest()
}
@JvmOverloads
public actual fun ByteArray.sha1(offset: Int, length: Int): ByteArray {
checkOffsetAndLength(offset, length)
return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest()
}
@JvmOverloads
public actual fun ByteArray.ungzip(offset: Int, length: Int): ByteArray {
return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() }
}
@JvmOverloads
public actual fun ByteArray.gzip(offset: Int, length: Int): ByteArray {
ByteArrayOutputStream().use { buf ->
GZIPOutputStream(buf).use { gzip ->
inputStream(offset, length).use { t -> t.copyTo(gzip) }
}
buf.flush()
return buf.toByteArray()
}
}
@JvmOverloads
public actual fun ByteArray.zip(offset: Int, length: Int): ByteArray {
checkOffsetAndLength(offset, length)
if (length == 0) return ByteArray(0)
val deflater = Deflater()
deflater.setInput(this, offset, length)
deflater.finish()
ByteArray(DEFAULT_BUFFER_SIZE).let {
return it.take(deflater.deflate(it)).toByteArray().also { deflater.end() }
}
}
public actual fun availableProcessors(): Int = Runtime.getRuntime().availableProcessors()

View File

@ -0,0 +1,20 @@
/*
* 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
internal actual fun hash(e: Throwable): Long {
return e.stackTrace.fold(0L) { acc, stackTraceElement ->
acc * 31 + hash(stackTraceElement).toLongUnsigned()
}
}
private fun hash(element: StackTraceElement): Int {
return element.lineNumber.hashCode() xor element.className.hashCode() xor element.methodName.hashCode()
}

View File

@ -0,0 +1,34 @@
/*
* 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("MiraiUtils")
package net.mamoe.mirai.utils
import java.io.File
public fun File.createFileIfNotExists() {
if (!this.exists()) {
this.parentFile.mkdirs()
this.createNewFile()
}
}
public fun File.resolveCreateFile(relative: String): File = this.resolve(relative).apply { createFileIfNotExists() }
public fun File.resolveCreateFile(relative: File): File = this.resolve(relative).apply { createFileIfNotExists() }
public fun File.resolveMkdir(relative: String): File = this.resolve(relative).apply { mkdirs() }
public fun File.resolveMkdir(relative: File): File = this.resolve(relative).apply { mkdirs() }
public fun File.touch(): File = apply {
parentFile?.mkdirs()
createNewFile()
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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,16 @@
/*
* 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
@TestOnly
public fun readResource(url: String): String =
Thread.currentThread().contextClassLoader.getResourceAsStream(url)?.readBytes()?.decodeToString()
?: error("Could not find resource '$url'")

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -13,7 +13,6 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.builtins.ByteArraySerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.serializer
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.atomic.AtomicIntegerFieldUpdater

View File

@ -0,0 +1,37 @@
/*
* 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 kotlinx.serialization.BinaryFormat
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.StringFormat
import java.io.File
public fun <T> File.loadNotBlankAs(
serializer: DeserializationStrategy<T>,
stringFormat: StringFormat,
): T? {
if (!this.exists() || this.length() == 0L) {
return null
}
return stringFormat.decodeFromString(serializer, this.readText())
}
public fun <T> File.loadNotBlankAs(
serializer: DeserializationStrategy<T>,
binaryFormat: BinaryFormat,
): T? {
if (!this.exists() || this.length() == 0L) {
return null
}
return binaryFormat.decodeFromByteArray(serializer, this.readBytes())
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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,12 @@
/*
* 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
public actual fun currentTimeMillis(): Long = System.currentTimeMillis()

View File

@ -1,10 +1,10 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 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. * 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/master/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmMultifileClass @file:JvmMultifileClass

View File

@ -0,0 +1,9 @@
/*
* 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

View File

@ -0,0 +1,12 @@
/*
* 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
internal actual fun getProperty(name: String, default: String): String? = System.getProperty(name, default)

View File

@ -0,0 +1,10 @@
/*
* 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

View File

@ -0,0 +1,17 @@
/*
* 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 kotlinx.io.errors.IOException
public actual interface Closeable {
@Throws(IOException::class)
public actual fun close()
}

View File

@ -0,0 +1,15 @@
/*
* 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
@Suppress("FunctionName")
public actual fun <K : Any, V> ConcurrentHashMap(): MutableMap<K, V> {
TODO("Not yet implemented")
}

View File

@ -0,0 +1,70 @@
/*
* 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
public actual suspend inline fun <R> runBIO(noinline block: () -> R): R {
return block()
}
public actual suspend inline fun <T, R> T.runBIO(crossinline block: T.() -> R): R {
TODO("Not yet implemented")
}
/**
* For code
* ```
* try {
* job(new)
* } catch (e: Throwable) {
* throw IllegalStateException("Exception in attached Job '$name'", e.unwrapCancellationException())
* }
* ```
*
* Original stacktrace, you mainly see `StateSwitchingException` which is useless to locate the code where real cause `ForceOfflineException` is thrown.
* ```
* Exception in thread "DefaultDispatcher-worker-1 @BotInitProcessor.init#7" java.lang.IllegalStateException: Exception in attached Job 'BotInitProcessor.init'
* at net.mamoe.mirai.internal.network.handler.state.JobAttachStateObserver$stateChanged0$1.invokeSuspend(JobAttachStateObserver.kt:40)
* at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
* at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
* at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
* Caused by: StateSwitchingException(old=StateLoading, new=StateClosed, cause=net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@4abf6d30)
* at net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport.setStateImpl$mirai_core(NetworkHandlerSupport.kt:258)
* at net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler.close(NettyNetworkHandler.kt:404)
* ```
*
* Real stacktrace (with [unwrapCancellationException]), you directly have `ForceOfflineException`, also you wont lose information of `StateSwitchingException`
* ```
* Exception in thread "DefaultDispatcher-worker-2 @BotInitProcessor.init#7" java.lang.IllegalStateException: Exception in attached Job 'BotInitProcessor.init'
* at net.mamoe.mirai.internal.network.handler.state.JobAttachStateObserver$stateChanged0$1.invokeSuspend(JobAttachStateObserver.kt:40)
* at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
* at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
* at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
* at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
* Caused by: net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@62f65f94
* at net.mamoe.mirai.utils.MiraiUtils__CoroutineUtilsKt.unwrapCancellationException(CoroutineUtils.kt:141)
* at net.mamoe.mirai.utils.MiraiUtils.unwrapCancellationException(Unknown Source)
* ... 7 more
* Suppressed: StateSwitchingException(old=StateLoading, new=StateClosed, cause=net.mamoe.mirai.internal.network.impl.netty.ForceOfflineException: Closed by MessageSvc.PushForceOffline: net.mamoe.mirai.internal.network.protocol.data.jce.RequestPushForceOffline@62f65f94)
* at net.mamoe.mirai.internal.network.handler.NetworkHandlerSupport.setStateImpl$mirai_core(NetworkHandlerSupport.kt:258)
* at net.mamoe.mirai.internal.network.impl.netty.NettyNetworkHandler.close(NettyNetworkHandler.kt:404)
* ```
*/
@Suppress("unused")
public actual inline fun <reified E> Throwable.unwrap(): Throwable {
if (this !is E) return this
return this.findCause { it !is E }
?.also { it.addSuppressed(this) }
?: this
}

Some files were not shown because too many files have changed in this diff Show More