Implement mirai-core for native

This commit is contained in:
Him188 2022-05-30 21:30:52 +01:00
parent 92222cf1e0
commit 13dadd5a95
127 changed files with 4383 additions and 2990 deletions

View File

@ -13,6 +13,7 @@ import org.gradle.api.Project
import org.gradle.api.attributes.Attribute
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getting
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME
@ -85,6 +86,8 @@ val LINUX_TARGETS = setOf("linuxX64")
val UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS }
val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
fun Project.configureHMPPJvm() {
extensions.getByType(KotlinMultiplatformExtension::class.java).apply {
@ -277,6 +280,16 @@ fun KotlinMultiplatformExtension.configureNativeTargetsHierarchical(
}
}
// Workaround from https://youtrack.jetbrains.com/issue/KT-52433/KotlinNative-Unable-to-generate-framework-with-Kotlin-1621-and-Xcode-134#focus=Comments-27-6140143.0-0
project.tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink>().configureEach {
val properties = listOf(
"ios_arm32", "watchos_arm32", "watchos_x86"
).joinToString(separator = ";") { "clangDebugFlags.$it=-Os" }
kotlinOptions.freeCompilerArgs += listOf(
"-Xoverride-konan-properties=$properties"
)
}
jvmBaseMain.dependsOn(commonMain)
jvmBaseTest.dependsOn(commonTest)

View File

@ -31,7 +31,7 @@ object Versions {
const val coroutines = "1.6.2"
const val atomicFU = "0.17.2"
const val serialization = "1.3.2"
const val ktor = "1.6.7"
const val ktor = "1.6.8"
const val binaryValidator = "0.4.0"

View File

@ -23,4 +23,6 @@ gnsp.disableApplyOnlyOnRootProjectEnforcement=true
mirai.android.target.api.level=24
# Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions.
systemProp.use.maven.local=false
org.gradle.caching=true
org.gradle.caching=true
kotlin.native.ignoreIncorrectDependencies=true
kotlin.mpp.enableCInteropCommonization=true

View File

@ -46,6 +46,7 @@ internal object PluginDataRenameToIdTest : AbstractTestPointAsPlugin() {
test: a
""".trimIndent()
)
File("data/PluginDataRenameToIdTest").mkdirs()
File("data/PluginDataRenameToIdTest/test.txt").createNewFile()
File("data/PluginDataRenameToIdTest/testdata.yml").writeText(
"""

View File

@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir
public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
}
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder {
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit {
public fun <init> (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object;
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void;
public abstract fun default (Lkotlin/jvm/functions/Function3;)V
public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V
public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V
public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope;
protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred;
public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V
public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V
public fun reply-8NSq9Eo (JLjava/lang/String;)V
public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V
public final fun timeout (JLkotlin/jvm/functions/Function1;)V
public final fun timeout-1WcQj8o (J)J
public final synthetic fun timeout-ncvN2qU (J)J
public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V
public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}
public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker {
@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain {
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
public synthetic fun add (ILjava/lang/Object;)V
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
public synthetic fun add (Ljava/lang/Object;)Z
public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun addAll (ILjava/util/Collection;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public final fun contains (Ljava/lang/Object;)Z
public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun contentToString ()Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z
public fun getSize ()I
public fun hashCode ()I
public final fun indexOf (Ljava/lang/Object;)I
public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun lastIndexOf (Ljava/lang/Object;)I
public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)Ljava/util/ListIterator;
public synthetic fun remove (I)Ljava/lang/Object;
public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun replaceAll (Ljava/util/function/UnaryOperator;)V
public fun retainAll (Ljava/util/Collection;)Z
public fun serializeToMiraiCode ()Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage;
public final fun size ()I
public fun sort (Ljava/util/Comparator;)V
public fun subList (II)Ljava/util/List;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}
@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
}
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getCause ()Ljava/lang/Throwable;
}
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/
public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun error0 (Ljava/lang/String;)V
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
public final fun info (Ljava/lang/String;)V
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun info0 (Ljava/lang/String;)V
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun isEnabled ()Z
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
public final fun verbose (Ljava/lang/String;)V
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun verbose0 (Ljava/lang/String;)V
@ -6466,7 +6423,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util
public fun error (Ljava/lang/String;)V
public fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun error (Ljava/lang/Throwable;)V
public synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
public fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
public fun getIdentity ()Ljava/lang/String;
public fun info (Ljava/lang/String;)V
public fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
@ -6478,7 +6435,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util
public fun isVerboseEnabled ()Z
public fun isWarningEnabled ()Z
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
public synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
public fun verbose (Ljava/lang/String;)V
public fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun verbose (Ljava/lang/Throwable;)V

View File

@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir
public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
}
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder {
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit {
public fun <init> (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object;
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void;
public abstract fun default (Lkotlin/jvm/functions/Function3;)V
public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V
public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V
public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope;
protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred;
public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V
public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V
public fun reply-8NSq9Eo (JLjava/lang/String;)V
public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V
public final fun timeout (JLkotlin/jvm/functions/Function1;)V
public final fun timeout-1WcQj8o (J)J
public final synthetic fun timeout-ncvN2qU (J)J
public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V
public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
}
public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker {
@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain {
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
public synthetic fun add (ILjava/lang/Object;)V
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
public synthetic fun add (Ljava/lang/Object;)Z
public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun addAll (ILjava/util/Collection;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public final fun contains (Ljava/lang/Object;)Z
public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun contentToString ()Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z
public fun getSize ()I
public fun hashCode ()I
public final fun indexOf (Ljava/lang/Object;)I
public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun lastIndexOf (Ljava/lang/Object;)I
public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)Ljava/util/ListIterator;
public synthetic fun remove (I)Ljava/lang/Object;
public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun replaceAll (Ljava/util/function/UnaryOperator;)V
public fun retainAll (Ljava/util/Collection;)Z
public fun serializeToMiraiCode ()Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage;
public final fun size ()I
public fun sort (Ljava/util/Comparator;)V
public fun subList (II)Ljava/util/List;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
public fun toString ()Ljava/lang/String;
}
@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
}
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun getCause ()Ljava/lang/Throwable;
}
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/
public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun error0 (Ljava/lang/String;)V
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
public final fun info (Ljava/lang/String;)V
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun info0 (Ljava/lang/String;)V
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun isEnabled ()Z
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
public final fun verbose (Ljava/lang/String;)V
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun verbose0 (Ljava/lang/String;)V

View File

@ -0,0 +1,125 @@
/*
* 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 android.util
import net.mamoe.mirai.internal.utils.StdoutLogger
// Dummy implementation for tests, since we don't have a SDK
@Suppress("UNUSED_PARAMETER", "unused")
object Log {
const val VERBOSE = 2
const val DEBUG = 3
const val INFO = 4
const val WARN = 5
const val ERROR = 6
const val ASSERT = 7
private val stdout = StdoutLogger("AndroidLog")
@JvmStatic
fun v(tag: String?, msg: String?): Int {
stdout.verbose(msg)
return 0
}
@JvmStatic
fun v(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.verbose(msg, tr)
return 0
}
@JvmStatic
fun d(tag: String?, msg: String?): Int {
stdout.debug(msg, tr)
return 0
}
@JvmStatic
fun d(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.debug(msg, tr)
return 0
}
@JvmStatic
fun i(tag: String?, msg: String?): Int {
stdout.info(msg, tr)
return 0
}
@JvmStatic
fun i(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.info(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, msg: String?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, tr: Throwable?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun e(tag: String?, msg: String?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun e(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, msg: String?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun getStackTraceString(tr: Throwable): String {
return tr.stackTraceToString()
}
@JvmStatic
fun println(priority: Int, tag: String?, msg: String?): Int {
stdout.info(msg, tr)
return 0
}
private inline val tr get() = null
private inline val msg get() = null
}

View File

@ -7,12 +7,4 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal.utils
import net.mamoe.mirai.contact.Group
internal actual class RemoteFileImpl actual constructor(contact: Group, path: String) :
CommonRemoteFileImpl(contact, path) {
actual constructor(contact: Group, parent: String, name: String) : this(contact, FileSystem.normalize(parent, name))
}
package net.mameo.mirai

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 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -11,7 +11,6 @@
package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.NotStableForInheritance
/**
@ -24,17 +23,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
* @see RemoteFiles
*/
@NotStableForInheritance
public interface FileSupported : Contact {
/**
* 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表.
*
* @since 2.5
*/
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public val filesRoot: net.mamoe.mirai.utils.RemoteFile
public expect interface FileSupported : Contact {
/**
* 获取远程文件列表 (管理器).
*

View File

@ -7,18 +7,13 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmBlockingBridge
@file:Suppress("OVERLOADS_INTERFACE")
package net.mamoe.mirai.contact.file
import kotlinx.coroutines.flow.Flow
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.PermissionDeniedException
import net.mamoe.mirai.utils.ExternalResource
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.ProgressionCallback
import kotlin.jvm.JvmOverloads
/**
* 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响.
@ -106,7 +101,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder {
/**
* 精确获取 [AbsoluteFile.id] [id] 的文件. 在目标文件不存在时返回 `null`. [deep] `true` 时还会深入子目录查找.
*/
@JvmOverloads
public suspend fun resolveFileById(
id: String,
deep: Boolean = false
@ -143,7 +137,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder {
*
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
@JvmOverloads
public suspend fun uploadNewFile(
filepath: String,
content: ExternalResource,

View File

@ -7,12 +7,9 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmBlockingBridge
package net.mamoe.mirai.contact.roaming
import kotlinx.coroutines.flow.Flow
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource

View File

@ -266,16 +266,6 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
*/
public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel<BaseEvent>
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
* @see context
* @since 2.12
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public fun exceptionHandler(coroutineExceptionHandler: Consumer<Throwable>): EventChannel<BaseEvent> {
return exceptionHandler { coroutineExceptionHandler.accept(it) }
}
/**
* [coroutineScope] 作为这个 [EventChannel] 的父作用域.

View File

@ -142,6 +142,7 @@ public suspend inline fun <reified E : Event, R : Any> EventChannel<*>.syncFromE
}
}
// Can't move to JVM, filename clashes
/**
* @since 2.10

View File

@ -0,0 +1,214 @@
/*
* 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.event
import kotlinx.coroutines.*
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.utils.MiraiInternalApi
/**
* [selectMessagesUnit] [selectMessages] 时的 DSL 构建器.
*
* 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL
*
* @see MessageSubscribersBuilder 查看上层 API
*/
public expect abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal constructor(
ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : CommonMessageSelectBuilderUnit<M, R>
/**
* [MessageSelectBuilderUnit] 的跨平台实现
*/
@MiraiInternalApi
public abstract class CommonMessageSelectBuilderUnit<M : MessageEvent, R> protected constructor(
private val ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : MessageSubscribersBuilder<M, Unit, R, Any?>(stub, subscriber) {
/**
* 当其他条件都不满足时的默认处理.
*/
@MessageDsl
public abstract fun default(onEvent: MessageListener<M, R>) // 需要后置默认监听器
@Deprecated("Use `default` instead", level = DeprecationLevel.HIDDEN)
override fun always(onEvent: MessageListener<M, Any?>): Nothing = error("prohibited")
/**
* 限制本次 select 的最长等待时间, 当超时后抛出 [TimeoutCancellationException]
*/
@Suppress("NOTHING_TO_INLINE")
@MessageDsl
public fun timeoutException(
timeoutMillis: Long,
exception: () -> Throwable
) {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
obtainCurrentCoroutineScope().launch {
delay(timeoutMillis)
val deferred = obtainCurrentDeferred() ?: return@launch
if (deferred.isActive && !deferred.isCompleted) {
deferred.completeExceptionally(exception())
}
}
}
/**
* 限制本次 select 的最长等待时间, 当超时后执行 [block] 以完成 select
*/
@MessageDsl
public fun timeout(timeoutMillis: Long, block: suspend () -> R) {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
obtainCurrentCoroutineScope().launch {
delay(timeoutMillis)
val deferred = obtainCurrentDeferred() ?: return@launch
if (deferred.isActive && !deferred.isCompleted) {
deferred.complete(block())
}
}
}
/**
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
*
* @see invoke
* @see reply
*/
@MessageDsl
public fun timeout(timeoutMillis: Long): MessageSelectionTimeoutChecker {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
return MessageSelectionTimeoutChecker(timeoutMillis)
}
/**
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
*
* @see Deferred<Unit>.invoke
*/
@Suppress("unused")
public fun MessageSelectionTimeoutChecker.invoke(block: suspend () -> R) {
return timeout(this.timeoutMillis, block)
}
/**
* 在超时后回复原消息
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*
* @see timeout
* @see quoteReply
*/
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(block: suspend () -> Any?) {
return timeout(this.timeoutMillis) {
executeAndReply(block)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(message: Message) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(message)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(message: String) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(message)
Unit as R
}
}
/**
* 在超时后引用回复原消息
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*
* @see timeout
* @see reply
*/
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(block: suspend () -> Any?) {
return timeout(this.timeoutMillis) {
executeAndQuoteReply(block)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: Message) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: String) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
Unit as R
}
}
/**
* 当其他条件都不满足时回复原消息.
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*/
@MessageDsl
public fun defaultReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
this@CommonMessageSelectBuilderUnit.executeAndReply(block)
})
/**
* 当其他条件都不满足时引用回复原消息.
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*/
@MessageDsl
public fun defaultQuoteReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
this@CommonMessageSelectBuilderUnit.executeAndQuoteReply(block)
})
private suspend inline fun executeAndReply(noinline block: suspend () -> Any?) {
when (val result = block()) {
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(result)
else -> ownerMessagePacket.subject.sendMessage(result.toString())
}
}
private suspend inline fun executeAndQuoteReply(noinline block: suspend () -> Any?) {
when (val result = block()) {
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result)
else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString())
}
}
protected abstract fun obtainCurrentCoroutineScope(): CoroutineScope
protected abstract fun obtainCurrentDeferred(): CompletableDeferred<R>?
}

View File

@ -25,7 +25,6 @@ import net.mamoe.mirai.event.AbstractEvent
import net.mamoe.mirai.internal.event.VerboseEvent
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.MiraiInternalApi
import kotlin.jvm.JvmField
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
@ -83,7 +82,6 @@ public data class NewFriendRequestEvent @MiraiInternalApi public constructor(
*/
public val fromNick: String,
) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent {
@JvmField
internal val responded: AtomicBoolean = atomic(false)
/**

View File

@ -29,7 +29,10 @@ import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi
import kotlin.jvm.*
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* 机器人被踢出群或在其他客户端主动退出一个群. 在事件广播前 [Bot.groups] 就已删除这个群.
@ -354,7 +357,6 @@ public data class BotInvitedJoinGroupRequestEvent @MiraiInternalApi constructor(
*/
public val invitor: Friend? get() = this.bot.getFriend(invitorId)
@JvmField
internal val responded: AtomicBoolean = atomic(false)
@JvmBlockingBridge
@ -403,8 +405,6 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor(
*/
public val invitor: NormalMember? by lazy { invitorId?.let { group?.get(it) } }
@JvmField
@PublishedApi
internal val responded: AtomicBoolean = atomic(false)
/**

View File

@ -14,8 +14,6 @@ package net.mamoe.mirai.event
import kotlinx.coroutines.*
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.Message
import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.isContextIdenticalWith
import net.mamoe.mirai.message.nextMessage
import net.mamoe.mirai.utils.MiraiExperimentalApi
@ -207,294 +205,6 @@ public abstract class MessageSelectBuilder<M : MessageEvent, R> @PublishedApi in
override fun ListeningFilter.quoteReply(replier: suspend M.(String) -> Any?): Nothing = error("prohibited")
}
/**
* [selectMessagesUnit] [selectMessages] 时的 DSL 构建器.
*
* 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL
*
* @see MessageSubscribersBuilder 查看上层 API
*/
public abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal constructor(
private val ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : MessageSubscribersBuilder<M, Unit, R, Any?>(stub, subscriber) {
/**
* 当其他条件都不满足时的默认处理.
*/
@MessageDsl
public abstract fun default(onEvent: MessageListener<M, R>) // 需要后置默认监听器
@Deprecated("Use `default` instead", level = DeprecationLevel.HIDDEN)
override fun always(onEvent: MessageListener<M, Any?>): Nothing = error("prohibited")
/**
* 限制本次 select 的最长等待时间, 当超时后抛出 [TimeoutCancellationException]
*/
@Suppress("NOTHING_TO_INLINE")
@MessageDsl
public fun timeoutException(
timeoutMillis: Long,
exception: () -> Throwable = { throw MessageSelectionTimeoutException() }
) {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
obtainCurrentCoroutineScope().launch {
delay(timeoutMillis)
val deferred = obtainCurrentDeferred() ?: return@launch
if (deferred.isActive && !deferred.isCompleted) {
deferred.completeExceptionally(exception())
}
}
}
/**
* 限制本次 select 的最长等待时间, 当超时后执行 [block] 以完成 select
*/
@MessageDsl
public fun timeout(timeoutMillis: Long, block: suspend () -> R) {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
obtainCurrentCoroutineScope().launch {
delay(timeoutMillis)
val deferred = obtainCurrentDeferred() ?: return@launch
if (deferred.isActive && !deferred.isCompleted) {
deferred.complete(block())
}
}
}
/**
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
*
* @see invoke
* @see reply
*/
@MessageDsl
public fun timeout(timeoutMillis: Long): MessageSelectionTimeoutChecker {
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
return MessageSelectionTimeoutChecker(timeoutMillis)
}
/**
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
*
* @see Deferred<Unit>.invoke
*/
@Suppress("unused")
public fun MessageSelectionTimeoutChecker.invoke(block: suspend () -> R) {
return timeout(this.timeoutMillis, block)
}
/**
* 在超时后回复原消息
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*
* @see timeout
* @see quoteReply
*/
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(block: suspend () -> Any?) {
return timeout(this.timeoutMillis) {
executeAndReply(block)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(message: Message) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(message)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.reply(message: String) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(message)
Unit as R
}
}
/**
* 在超时后引用回复原消息
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*
* @see timeout
* @see reply
*/
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(block: suspend () -> Any?) {
return timeout(this.timeoutMillis) {
executeAndQuoteReply(block)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: Message) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
Unit as R
}
}
@Suppress("unused", "UNCHECKED_CAST")
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: String) {
return timeout(this.timeoutMillis) {
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
Unit as R
}
}
/**
* 当其他条件都不满足时回复原消息.
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*/
@MessageDsl
public fun defaultReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
this@MessageSelectBuilderUnit.executeAndReply(block)
})
/**
* 当其他条件都不满足时引用回复原消息.
*
* [block] 返回值为 [Unit] 时不回复, [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
*/
@MessageDsl
public fun defaultQuoteReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
this@MessageSelectBuilderUnit.executeAndQuoteReply(block)
})
private suspend inline fun executeAndReply(noinline block: suspend () -> Any?) {
when (val result = block()) {
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(result)
else -> ownerMessagePacket.subject.sendMessage(result.toString())
}
}
private suspend inline fun executeAndQuoteReply(noinline block: suspend () -> Any?) {
when (val result = block()) {
Unit -> {
}
is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result)
else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString())
}
}
@JvmName("timeout-ncvN2qU")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker {
return timeout(timeoutMillis)
}
@Suppress("unused")
@JvmName("invoke-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) {
return invoke(block)
}
@Suppress("unused")
@JvmName("invoke-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? {
invoke(block)
return null
}
@JvmName("reply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) {
return reply(block)
}
@JvmName("reply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? {
reply(block)
return null
}
@JvmName("reply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(message: String) {
return reply(message)
}
@JvmName("reply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? {
reply(message)
return null
}
@JvmName("reply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) {
return reply(message)
}
@JvmName("reply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? {
reply(message)
return null
}
@JvmName("quoteReply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) {
return reply(block)
}
@JvmName("quoteReply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? {
reply(block)
return null
}
@JvmName("quoteReply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) {
return reply(message)
}
@JvmName("quoteReply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? {
reply(message)
return null
}
@JvmName("quoteReply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) {
return reply(message)
}
@JvmName("quoteReply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? {
reply(message)
return null
}
protected abstract fun obtainCurrentCoroutineScope(): CoroutineScope
protected abstract fun obtainCurrentDeferred(): CompletableDeferred<R>?
}
@JvmInline
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
public value class MessageSelectionTimeoutChecker internal constructor(public val timeoutMillis: Long)

View File

@ -22,9 +22,11 @@ import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.copy
import net.mamoe.mirai.utils.map
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
@ -46,10 +48,11 @@ import kotlin.jvm.JvmSynthetic
* @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定.
*/
@Serializable(FileMessage.Serializer::class)
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
@SerialName(FileMessage.SERIAL_NAME)
@NotStableForInheritance
@JvmBlockingBridge
public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/**
* 服务器需要的某种 ID.
*/
@ -70,76 +73,58 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
*/
public val size: Long
override fun contentToString(): String = "[文件]$name" // orthodox
open override fun contentToString(): String
override fun appendMiraiCodeTo(builder: StringBuilder) {
builder.append("[mirai:file:")
builder.appendStringAsMiraiCode(id).append(",")
builder.append(internalId).append(",")
builder.appendStringAsMiraiCode(name).append(",")
builder.append(size).append("]")
}
open override fun appendMiraiCodeTo(builder: StringBuilder)
/**
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*/
@Suppress("DEPRECATION")
@Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? {
@Suppress("DEPRECATION")
return contact.filesRoot.resolveById(id)
}
/**
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
* 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*
* @since 2.8
*/
public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
override val key: Key get() = Key
open override val key: Key
@MiraiInternalApi
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitFileMessage(this, data)
}
open override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R
/**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/
public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
MessageContent, { it.safeCast() }) {
AbstractPolymorphicMessageKey<MessageContent, FileMessage> {
public const val SERIAL_NAME: String = "FileMessage"
@Suppress("CONST_VAL_WITHOUT_INITIALIZER")
public const val SERIAL_NAME: String
/**
* 构造 [FileMessage]
* @since 2.5
*/
@JvmStatic
public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage =
Mirai.createFileMessage(id, internalId, name, size)
public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage
}
public object Serializer : KSerializer<FileMessage> by FallbackSerializer(SERIAL_NAME) // not polymorphic
public object Serializer : KSerializer<FileMessage> // not polymorphic
}
@MiraiInternalApi
private open class FallbackSerializer(serialName: String) : KSerializer<FileMessage> by Delegate.serializer().map(
@MiraiInternalApi
internal open class FallbackFileMessageSerializer constructor(serialName: String) :
KSerializer<FileMessage> by Delegate.serializer().map(
Delegate.serializer().descriptor.copy(serialName),
serialize = { Delegate(id, internalId, name, size) },
deserialize = { Mirai.createFileMessage(id, internalId, name, size) },
) {
@SerialName(SERIAL_NAME)
@Serializable
data class Delegate(
val id: String,
val internalId: Int,
val name: String,
val size: Long,
)
}
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
@SerialName(FileMessage.SERIAL_NAME)
@Serializable
data class Delegate constructor(
val id: String,
val internalId: Int,
val name: String,
val size: Long,
)
}
/**

View File

@ -21,7 +21,6 @@ import net.mamoe.mirai.event.EventPriority
import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.event.syncFromEvent
import net.mamoe.mirai.event.syncFromEventOrNull
import net.mamoe.mirai.message.data.MessageChain
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -74,7 +73,7 @@ public suspend inline fun <reified P : MessageEvent> P.nextMessage(
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值
* @return 消息链. 超时时返回 `null`
*
* @see syncFromEventOrNull 实现原理
* @see syncFromEvent
*/
@JvmSynthetic
public suspend inline fun <reified P : MessageEvent> P.nextMessageOrNull(

View File

@ -1,584 +0,0 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("unused", "DEPRECATION")
package net.mamoe.mirai.utils
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
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.utils.RemoteFile.Companion.uploadFile
import net.mamoe.mirai.utils.RemoteFile.ProgressionCallback.Companion.asProgressionCallback
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
/**
* 表示一个远程文件或目录.
*
* [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 expect interface RemoteFile {
/**
* 文件名或目录名.
*/
public val name: String
/**
* 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
*/
public val id: String?
/**
* 标准的绝对路径, 起始字符为 '/'. `/foo/bar.txt`.
*
* 根目录路径为 [ROOT_PATH]
*/
public val path: String
/**
* 获取父目录, [RemoteFile] 表示根目录时返回 `null`
*/
public val parent: RemoteFile?
/**
* 此文件所属的群或好友
*/
public val contact: FileSupported
/**
* [RemoteFile] 表示一个文件时返回 `true`.
*/
public suspend fun isFile(): Boolean
/**
* [RemoteFile] 表示一个目录时返回 `true`.
*/
public open suspend fun isDirectory(): Boolean
/**
* 获取文件长度. [RemoteFile] 表示一个目录时行为不确定.
*/
public suspend fun length(): Long
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 id: String
/**
* 标准绝对路径.
*/
public val path: String
/**
* 文件长度 (大小) bytes, 目录的 [length] 0.
*/
public val length: Long
/**
* 下载次数. 目录没有下载次数, 此属性总是 `0`.
*/
public val downloadTimes: Int
/**
* 上传者 ID. 目录没有上传者, 此属性总是 `0`.
*/
public val uploaderId: Long
/**
* 上传的时间. 目录没有上传时间, 此属性总是 `0`.
*/
public val uploadTime: Long
/**
* 上次修改时间. 时间戳秒.
*/
public val lastModifyTime: Long
public val sha1: ByteArray
public val md5: ByteArray
/**
* 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile].
*/
public suspend fun resolveToFile(contact: FileSupported): RemoteFile
}
/**
* 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
*/
public suspend fun getInfo(): FileInfo?
/**
* 当文件或目录存在时返回 `true`.
*/
public suspend fun exists(): Boolean
/**
* @return [path]
*/
public override fun toString(): String
///////////////////////////////////////////////////////////////////////////
// resolve
///////////////////////////////////////////////////////////////////////////
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public fun resolve(relative: String): RemoteFile
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
*
* @param relative 相对路径. [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public fun resolve(relative: RemoteFile): RemoteFile
/**
* 获取该目录下的 ID [id] 的文件, [deep] `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
* @see resolve
*/
public suspend fun resolveById(id: String, deep: Boolean = true): RemoteFile?
/**
* 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null`
* @see resolve
*/
public open suspend fun resolveById(id: String): RemoteFile?
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
* 不会检查 [RemoteFile] 是否表示一个目录.
*
* @param relative 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public 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 fun resolveSibling(relative: RemoteFile): RemoteFile
///////////////////////////////////////////////////////////////////////////
// operations
///////////////////////////////////////////////////////////////////////////
/**
* 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*/
public suspend fun delete(): Boolean
/**
* 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
* 操作非 Bot 自己上传的文件时需要管理员权限.
*
* [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*/
public 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 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 open suspend fun moveTo(path: String): Boolean
/**
* 创建目录. 目录已经存在或无管理员权限时返回 `false`.
*
* 创建后 [isDirectory] 也不一定会返回 `true`.
* [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] [isDirectory] 结果取决于服务器.
*/
public suspend fun mkdir(): Boolean
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyFlow].
*
* 返回的 [Flow] **, 只会在被需要的时候向服务器查询.
*/
public suspend fun listFiles(): Flow<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回空迭代器.
* @param lazy `true` 时惰性获取, `false` 时立即获取全部文件列表.
*/
@JavaFriendlyAPI
public suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList].
*/
public open suspend fun listFilesCollection(): List<RemoteFile>
/**
* 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
*/
public 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 interface ProgressionCallback {
/**
* 当上传开始时调用
*/
public open fun onBegin(file: RemoteFile, resource: ExternalResource)
/**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
*
* 提示: 可通过 [ExternalResource.size] 获取文件总大小.
*/
public open fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long)
/**
* 当上传成功时调用
*/
public open fun onSuccess(file: RemoteFile, resource: ExternalResource)
/**
* 当上传以异常失败时调用
*/
public open fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable)
public 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 fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback
}
}
/**
* 上传文件到 [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 suspend fun upload(
resource: ExternalResource,
callback: ProgressionCallback? = null,
): 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 open suspend fun upload(resource: ExternalResource): FileMessage
/**
* 上传文件并发送文件消息.
*
* [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件.
* 即使用 [resolve] [resolveSibling] 获取到的 [RemoteFile] [upload] 总是上传一个新文件,
* 而使用 [resolveById] [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件.
*
* @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource].
* @see upload
*/
@MiraiExperimentalApi
public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
/**
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
*/
public suspend fun getDownloadInfo(): DownloadInfo?
public class DownloadInfo @MiraiInternalApi constructor(
filename: String,
id: String,
path: String,
url: String,
sha1: ByteArray,
md5: ByteArray,
) {
/**
* @see RemoteFile.name
*/
public val filename: String
/**
* @see RemoteFile.id
*/
public val id: String
/**
* 标准绝对路径
* @see RemoteFile.path
*/
public val path: String
/**
* HTTP or HTTPS URL
*/
public val url: String
public val sha1: ByteArray
public val md5: ByteArray
override fun toString(): String
}
public companion object {
/**
* 根目录路径
* @see RemoteFile.path
*/
@Suppress("CONST_VAL_WITHOUT_INITIALIZER") // compiler bug
public 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 suspend fun FileSupported.uploadFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback? = null,
): FileMessage
/**
* 上传文件并发送文件消息到相关 [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 suspend fun <C : FileSupported> C.sendFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback? = null,
): MessageReceipt<C>
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 支持文件操作的 [Contact]. 目前仅 [Group].
*
* 获取文件操作相关示例: [RemoteFiles]
*
* @since 2.5
*
* @see RemoteFiles
*/
@NotStableForInheritance
public actual interface FileSupported : Contact {
/**
* 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表.
*
* @since 2.5
*/
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public val filesRoot: net.mamoe.mirai.utils.RemoteFile
/**
* 获取远程文件列表 (管理器).
*
* @since 2.8
*/
public actual val files: RemoteFiles
}

View File

@ -131,9 +131,11 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder {
/**
* 精确获取 [AbsoluteFile.id] [id] 的文件. 在目标文件不存在时返回 `null`. [deep] `true` 时还会深入子目录查找.
*/
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
@JvmOverloads
public actual suspend fun resolveFileById(
id: String,
deep: Boolean
deep: Boolean = false
): AbsoluteFile?
/**
@ -187,10 +189,12 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder {
*
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
@JvmOverloads
public actual suspend fun uploadNewFile(
filepath: String,
content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?,
callback: ProgressionCallback<AbsoluteFile, Long>? = null,
): AbsoluteFile
public actual companion object {

View File

@ -7,9 +7,12 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmBlockingBridge
package net.mamoe.mirai.contact.roaming
import kotlinx.coroutines.flow.Flow
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource
@ -46,10 +49,11 @@ public actual interface RoamingMessages {
* @param timeEnd 结束时间, UTC+8 时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值.
* @param filter 过滤器.
*/
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
public actual suspend fun getMessagesIn(
timeStart: Long,
timeEnd: Long,
filter: RoamingMessageFilter?
filter: RoamingMessageFilter? = null
): Flow<MessageChain>
/**
@ -68,8 +72,9 @@ public actual interface RoamingMessages {
*
* @param filter 过滤器.
*/
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
public actual suspend fun getAllMessages(
filter: RoamingMessageFilter?
filter: RoamingMessageFilter? = null
): Flow<MessageChain> = getMessagesIn(0, Long.MAX_VALUE, filter)
/**

View File

@ -309,6 +309,17 @@ public actual abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
})
}
/**
* 创建一个新的 [EventChannel], [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
* @see context
* @since 2.12
*/
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
public fun exceptionHandler(coroutineExceptionHandler: Consumer<Throwable>): EventChannel<BaseEvent> {
return exceptionHandler { coroutineExceptionHandler.accept(it) }
}
/**
* [coroutineScope] 作为这个 [EventChannel] 的父作用域.
*

View File

@ -0,0 +1,126 @@
/*
* 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.event
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.Message
/**
* [selectMessagesUnit] [selectMessages] 时的 DSL 构建器.
*
* 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL
*
* @see MessageSubscribersBuilder 查看上层 API
*/
public actual abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal actual constructor(
ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : CommonMessageSelectBuilderUnit<M, R>(ownerMessagePacket, stub, subscriber) {
@JvmName("timeout-ncvN2qU")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker {
return timeout(timeoutMillis)
}
@Suppress("unused")
@JvmName("invoke-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) {
return invoke(block)
}
@Suppress("unused")
@JvmName("invoke-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? {
invoke(block)
return null
}
@JvmName("reply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) {
return reply(block)
}
@JvmName("reply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? {
reply(block)
return null
}
@JvmName("reply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(message: String) {
return reply(message)
}
@JvmName("reply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? {
reply(message)
return null
}
@JvmName("reply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) {
return reply(message)
}
@JvmName("reply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? {
reply(message)
return null
}
@JvmName("quoteReply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) {
return reply(block)
}
@JvmName("quoteReply-RNyhSv4")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? {
reply(block)
return null
}
@JvmName("quoteReply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) {
return reply(message)
}
@JvmName("quoteReply-sCZ5gAI")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? {
reply(message)
return null
}
@JvmName("quoteReply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) {
return reply(message)
}
@JvmName("quoteReply-AVDwu3U")
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? {
reply(message)
return null
}
}

View File

@ -17,8 +17,6 @@ import net.mamoe.mirai.Bot
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import kotlin.coroutines.resume
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import kotlin.reflect.KClass

View File

@ -17,8 +17,6 @@ import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.MiraiExperimentalApi
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
/**

View File

@ -16,8 +16,6 @@ import kotlinx.coroutines.*
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import kotlin.reflect.KClass
/**

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
*/
package net.mamoe.mirai.message.data
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.*
/**
* 文件消息.
*
* [name] [size] 只供本地使用, 发送消息时只会使用 [id] [internalId].
*
* : [FileMessage] 不可二次发送
*
* ### 文件操作
* 要下载这个文件, 可通过 [toAbsoluteFile] 获取到 [AbsoluteFile] 然后操作.
*
* 要获取到 [FileMessage], 可以通过 [MessageEvent.message] 获取, 或通过 [AbsoluteFile.toMessage] 得到.
*
* @since 2.5
* @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定.
*/
@Serializable(FileMessage.Serializer::class)
@SerialName(FileMessage.SERIAL_NAME)
@NotStableForInheritance
@JvmBlockingBridge
public actual interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/**
* 服务器需要的某种 ID.
*/
public actual val id: String
/**
* 服务器需要的某种 ID.
*/
public actual val internalId: Int
/**
* 文件名
*/
public actual val name: String
/**
* 文件大小 bytes
*/
public actual val size: Long
actual override fun contentToString(): String = "[文件]$name" // orthodox
actual override fun appendMiraiCodeTo(builder: StringBuilder) {
builder.append("[mirai:file:")
builder.appendStringAsMiraiCode(id).append(",")
builder.append(internalId).append(",")
builder.appendStringAsMiraiCode(name).append(",")
builder.append(size).append("]")
}
/**
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*/
@Suppress("DEPRECATION")
@Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? {
@Suppress("DEPRECATION")
return contact.filesRoot.resolveById(id)
}
/**
* 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*
* @since 2.8
*/
public actual suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
actual override val key: Key get() = Key
@MiraiInternalApi
actual override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitFileMessage(this, data)
}
/**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/
public actual companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
MessageContent, { it.safeCast() }) {
public actual const val SERIAL_NAME: String = "FileMessage"
/**
* 构造 [FileMessage]
* @since 2.5
*/
@JvmStatic
public actual fun create(id: String, internalId: Int, name: String, size: Long): FileMessage =
Mirai.createFileMessage(id, internalId, name, size)
}
public actual object Serializer :
KSerializer<FileMessage> by FallbackFileMessageSerializer(SERIAL_NAME) // not polymorphic
}

View File

@ -60,26 +60,6 @@ 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
///////////////////////////////////////////////////////////////////////////
@ -341,7 +321,7 @@ public actual open class BotConfiguration { // open for Java
@ConfigurationDsl
public actual fun loadDeviceInfoJson(json: String) {
deviceInfo = {
this.json.decodeFromString(DeviceInfo.serializer(), json)
DeviceInfoManager.deserialize(json, Companion.json)
}
}
@ -603,7 +583,6 @@ public actual open class BotConfiguration { // open for Java
// To structural order
new.workingDir = workingDir
@Suppress("DEPRECATION_ERROR")
new.json = json
new.parentCoroutineContext = parentCoroutineContext
new.heartbeatPeriodMillis = heartbeatPeriodMillis
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
@ -646,6 +625,20 @@ public actual open class BotConfiguration { // open for Java
@JvmStatic
public actual val Default: BotConfiguration = BotConfiguration()
/**
* Json 序列化器, 使用 'kotlinx.serialization'
*/
internal val json: Json = kotlin.runCatching {
Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = true
}
}.getOrElse {
@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
Json {}
}
internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
return {
@Suppress("DEPRECATION_ERROR")

View File

@ -103,106 +103,106 @@ import java.io.File
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
@NotStableForInheritance
public actual interface RemoteFile {
public interface RemoteFile {
/**
* 文件名或目录名.
*/
public actual val name: String
public val name: String
/**
* 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
*/
public actual val id: String?
public val id: String?
/**
* 标准的绝对路径, 起始字符为 '/'. `/foo/bar.txt`.
*
* 根目录路径为 [ROOT_PATH]
*/
public actual val path: String
public val path: String
/**
* 获取父目录, [RemoteFile] 表示根目录时返回 `null`
*/
public actual val parent: RemoteFile?
public val parent: RemoteFile?
/**
* 此文件所属的群或好友
*/
public actual val contact: FileSupported
public val contact: FileSupported
/**
* [RemoteFile] 表示一个文件时返回 `true`.
*/
public actual suspend fun isFile(): Boolean
public suspend fun isFile(): Boolean
/**
* [RemoteFile] 表示一个目录时返回 `true`.
*/
public actual suspend fun isDirectory(): Boolean = !isFile()
public suspend fun isDirectory(): Boolean = !isFile()
/**
* 获取文件长度. [RemoteFile] 表示一个目录时行为不确定.
*/
public actual suspend fun length(): Long
public suspend fun length(): Long
public actual class FileInfo @MiraiInternalApi actual constructor(
public class FileInfo @MiraiInternalApi constructor(
/**
* 文件或目录名.
*/
public actual val name: String,
public val name: String,
/**
* 唯一识别标识.
*/
public actual val id: String,
public val id: String,
/**
* 标准绝对路径.
*/
public actual val path: String,
public val path: String,
/**
* 文件长度 (大小) bytes, 目录的 [length] 0.
*/
public actual val length: Long,
public val length: Long,
/**
* 下载次数. 目录没有下载次数, 此属性总是 `0`.
*/
public actual val downloadTimes: Int,
public val downloadTimes: Int,
/**
* 上传者 ID. 目录没有上传者, 此属性总是 `0`.
*/
public actual val uploaderId: Long,
public val uploaderId: Long,
/**
* 上传的时间. 目录没有上传时间, 此属性总是 `0`.
*/
public actual val uploadTime: Long,
public val uploadTime: Long,
/**
* 上次修改时间. 时间戳秒.
*/
public actual val lastModifyTime: Long,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
public val lastModifyTime: Long,
public val sha1: ByteArray,
public val md5: ByteArray,
) {
/**
* 根据 [FileInfo.id] [FileInfo.path] 获取到对应的 [RemoteFile].
*/
public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
public suspend fun resolveToFile(contact: FileSupported): RemoteFile =
contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
}
/**
* 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
*/
public actual suspend fun getInfo(): FileInfo?
public suspend fun getInfo(): FileInfo?
/**
* 当文件或目录存在时返回 `true`.
*/
public actual suspend fun exists(): Boolean
public suspend fun exists(): Boolean
/**
* @return [path]
*/
public actual override fun toString(): String
public override fun toString(): String
///////////////////////////////////////////////////////////////////////////
// resolve
@ -214,7 +214,7 @@ public actual interface RemoteFile {
* @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: String): RemoteFile
public fun resolve(relative: String): RemoteFile
/**
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
@ -222,19 +222,20 @@ public actual interface RemoteFile {
* @param relative 相对路径. [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolve stdlib 内的类似函数
*/
public actual fun resolve(relative: RemoteFile): RemoteFile
public fun resolve(relative: RemoteFile): RemoteFile
/**
* 获取该目录下的 ID [id] 的文件, [deep] `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
* @see resolve
*/
public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") // JVM ABI
public suspend fun resolveById(id: String, deep: Boolean = true): RemoteFile?
/**
* 获取该目录或子目录下的 ID [id] 的文件, 在不存在时返回 `null`
* @see resolve
*/
public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
public suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
@ -243,7 +244,7 @@ public actual interface RemoteFile {
* @param relative 当初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: String): RemoteFile
public fun resolveSibling(relative: String): RemoteFile
/**
* 获取父目录的子文件. `RemoteFile("/foo/bar").resolveSibling("gav")` `RemoteFile("/foo/gav")`.
@ -252,7 +253,7 @@ public actual interface RemoteFile {
* @param relative [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
* @see File.resolveSibling stdlib 内的类似函数
*/
public actual fun resolveSibling(relative: RemoteFile): RemoteFile
public fun resolveSibling(relative: RemoteFile): RemoteFile
///////////////////////////////////////////////////////////////////////////
// operations
@ -261,7 +262,7 @@ public actual interface RemoteFile {
/**
* 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
*/
public actual suspend fun delete(): Boolean
public suspend fun delete(): Boolean
/**
* 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
@ -269,7 +270,7 @@ public actual interface RemoteFile {
*
* [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
*/
public actual suspend fun renameTo(name: String): Boolean
public suspend fun renameTo(name: String): Boolean
/**
* 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
@ -287,7 +288,7 @@ public actual interface RemoteFile {
*
* @param target 目标文件位置.
*/
public actual suspend fun moveTo(target: RemoteFile): Boolean
public suspend fun moveTo(target: RemoteFile): Boolean
/**
* 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
@ -307,7 +308,7 @@ public actual interface RemoteFile {
level = DeprecationLevel.ERROR
) // deprecated since 2.7
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
public actual suspend fun moveTo(path: String): Boolean {
public 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.
@ -320,31 +321,31 @@ public actual interface RemoteFile {
* 创建后 [isDirectory] 也不一定会返回 `true`.
* [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] [isDirectory] 结果取决于服务器.
*/
public actual suspend fun mkdir(): Boolean
public suspend fun mkdir(): Boolean
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyFlow].
*
* 返回的 [Flow] **, 只会在被需要的时候向服务器查询.
*/
public actual suspend fun listFiles(): Flow<RemoteFile>
public suspend fun listFiles(): Flow<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回空迭代器.
* @param lazy `true` 时惰性获取, `false` 时立即获取全部文件列表.
*/
@JavaFriendlyAPI
public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
public suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
/**
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回 [emptyList].
*/
public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
public suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
/**
* 得到相应文件消息. [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
*/
public actual suspend fun toMessage(): FileMessage?
public suspend fun toMessage(): FileMessage?
///////////////////////////////////////////////////////////////////////////
// upload & download
@ -360,30 +361,30 @@ public actual interface RemoteFile {
level = DeprecationLevel.WARNING
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual interface ProgressionCallback {
public interface ProgressionCallback {
/**
* 当上传开始时调用
*/
public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
public fun onBegin(file: RemoteFile, resource: ExternalResource) {}
/**
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
*
* 提示: 可通过 [ExternalResource.size] 获取文件总大小.
*/
public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
/**
* 当上传成功时调用
*/
public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
public fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
/**
* 当上传以异常失败时调用
*/
public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
public actual companion object {
public companion object {
/**
* 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
*
@ -410,7 +411,7 @@ public actual interface RemoteFile {
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
*/
@JvmStatic
public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = false): ProgressionCallback {
return object : ProgressionCallback {
override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
trySend(downloadedSize)
@ -462,9 +463,10 @@ public actual interface RemoteFile {
"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(
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public suspend fun upload(
resource: ExternalResource,
callback: ProgressionCallback?,
callback: ProgressionCallback? = null,
): FileMessage
/**
@ -478,7 +480,7 @@ public actual interface RemoteFile {
"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)
public suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
/**
* 上传文件.
@ -520,7 +522,7 @@ public actual interface RemoteFile {
* @see upload
*/
@MiraiExperimentalApi
public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
/**
* 上传文件并发送文件消息.
@ -533,41 +535,41 @@ public actual interface RemoteFile {
/**
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
*/
public actual suspend fun getDownloadInfo(): DownloadInfo?
public suspend fun getDownloadInfo(): DownloadInfo?
public actual class DownloadInfo @MiraiInternalApi actual constructor(
public class DownloadInfo @MiraiInternalApi constructor(
/**
* @see RemoteFile.name
*/
public actual val filename: String,
public val filename: String,
/**
* @see RemoteFile.id
*/
public actual val id: String,
public val id: String,
/**
* 标准绝对路径
* @see RemoteFile.path
*/
public actual val path: String,
public val path: String,
/**
* HTTP or HTTPS URL
*/
public actual val url: String,
public actual val sha1: ByteArray,
public actual val md5: ByteArray,
public val url: String,
public val sha1: ByteArray,
public val md5: ByteArray,
) {
actual override fun toString(): String {
override fun toString(): String {
return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
"md5=${md5.toUHexString("")})"
}
}
public actual companion object {
public companion object {
/**
* 根目录路径
* @see RemoteFile.path
*/
public actual const val ROOT_PATH: String = "/"
public const val ROOT_PATH: String = "/"
/**
* 上传文件并获取文件消息, 但不发送.
@ -590,10 +592,11 @@ public actual interface RemoteFile {
level = DeprecationLevel.ERROR
) // deprecated since 2.7-M1
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
public actual suspend fun FileSupported.uploadFile(
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public suspend fun FileSupported.uploadFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
callback: ProgressionCallback? = null,
): FileMessage =
@Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
@ -633,13 +636,14 @@ public actual interface RemoteFile {
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
level = DeprecationLevel.WARNING
level = DeprecationLevel.ERROR
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
public actual suspend fun <C : FileSupported> C.sendFile(
@DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
public suspend fun <C : FileSupported> C.sendFile(
path: String,
resource: ExternalResource,
callback: ProgressionCallback?,
callback: ProgressionCallback? = null,
): MessageReceipt<C> =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
@ -653,9 +657,9 @@ public actual interface RemoteFile {
@Deprecated(
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
level = DeprecationLevel.WARNING
level = DeprecationLevel.ERROR
) // deprecated since 2.8.0-RC
@DeprecatedSinceMirai(warningSince = "2.8")
@DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
public suspend fun <C : FileSupported> C.sendFile(
path: String,
file: File,

View File

@ -0,0 +1,33 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 支持文件操作的 [Contact]. 目前仅 [Group].
*
* 获取文件操作相关示例: [RemoteFiles]
*
* @since 2.5
*
* @see RemoteFiles
*/
@NotStableForInheritance
public actual interface FileSupported : Contact {
/**
* 获取远程文件列表 (管理器).
*
* @since 2.8
*/
public actual val files: RemoteFiles
}

View File

@ -0,0 +1,25 @@
/*
* 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.event
import net.mamoe.mirai.event.events.MessageEvent
/**
* [selectMessagesUnit] [selectMessages] 时的 DSL 构建器.
*
* 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL
*
* @see MessageSubscribersBuilder 查看上层 API
*/
public actual abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal actual constructor(
ownerMessagePacket: M,
stub: Any?,
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
) : CommonMessageSelectBuilderUnit<M, R>(ownerMessagePacket, stub, subscriber)

View File

@ -0,0 +1,110 @@
/*
* 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.message.data
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Mirai
import net.mamoe.mirai.contact.FileSupported
import net.mamoe.mirai.contact.file.AbsoluteFile
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage
import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
import net.mamoe.mirai.message.data.visitor.MessageVisitor
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.NotStableForInheritance
import net.mamoe.mirai.utils.safeCast
/**
* 文件消息.
*
* [name] [size] 只供本地使用, 发送消息时只会使用 [id] [internalId].
*
* : [FileMessage] 不可二次发送
*
* ### 文件操作
* 要下载这个文件, 可通过 [toAbsoluteFile] 获取到 [AbsoluteFile] 然后操作.
*
* 要获取到 [FileMessage], 可以通过 [MessageEvent.message] 获取, 或通过 [AbsoluteFile.toMessage] 得到.
*
* @since 2.5
* @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定.
*/
@Serializable(FileMessage.Serializer::class)
@SerialName(FileMessage.SERIAL_NAME)
@NotStableForInheritance
@JvmBlockingBridge
public actual interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/**
* 服务器需要的某种 ID.
*/
public actual val id: String
/**
* 服务器需要的某种 ID.
*/
public actual val internalId: Int
/**
* 文件名
*/
public actual val name: String
/**
* 文件大小 bytes
*/
public actual val size: Long
actual override fun contentToString(): String = "[文件]$name" // orthodox
actual override fun appendMiraiCodeTo(builder: StringBuilder) {
builder.append("[mirai:file:")
builder.appendStringAsMiraiCode(id).append(",")
builder.append(internalId).append(",")
builder.appendStringAsMiraiCode(name).append(",")
builder.append(size).append("]")
}
/**
* 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`.
*
* @since 2.8
*/
public actual suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
actual override val key: Key get() = Key
@MiraiInternalApi
actual override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
return visitor.visitFileMessage(this, data)
}
/**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/
public actual companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
MessageContent, { it.safeCast() }) {
public actual const val SERIAL_NAME: String = "FileMessage"
/**
* 构造 [FileMessage]
* @since 2.5
*/
public actual fun create(id: String, internalId: Int, name: String, size: Long): FileMessage =
Mirai.createFileMessage(id, internalId, name, size)
}
public actual object Serializer :
KSerializer<FileMessage> by FallbackFileMessageSerializer(SERIAL_NAME) // not polymorphic
}

View File

@ -314,7 +314,7 @@ public actual open class BotConfiguration { // open for Java
@ConfigurationDsl
public actual fun loadDeviceInfoJson(json: String) {
deviceInfo = {
Companion.json.decodeFromString(DeviceInfo.serializer(), json)
DeviceInfoManager.deserialize(json, Companion.json)
}
}
@ -329,7 +329,10 @@ public actual open class BotConfiguration { // open for Java
public actual fun fileBasedDeviceInfo(filepath: String) {
deviceInfo = {
val file = MiraiFile.create(workingDir).resolve(filepath)
Json.decodeFromString(DeviceInfo.serializer(), file.readText())
if (!file.exists()) {
file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), json))
}
DeviceInfoManager.deserialize(file.readText(), json)
}
}

View File

@ -75,7 +75,7 @@ public actual abstract class LoginSolver actual constructor() {
* @return `SwingSolver` `StandardCharImageLoginSolver` `null`
*/
public actual val Default: LoginSolver?
get() = TODO("Not yet implemented")
get() = null
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
@Suppress("unused")

View File

@ -57,11 +57,11 @@ public actual interface MiraiLogger {
public actual companion object INSTANCE : Factory by loadService(Factory::class, fallbackImplementation = {
object : Factory {
override fun create(requester: KClass<*>): MiraiLogger {
return PlatformLogger(requester.qualifiedName ?: requester.simpleName)
return PlatformLogger(requester.simpleName ?: requester.qualifiedName)
}
override fun create(requester: KClass<*>, identity: String?): MiraiLogger {
return PlatformLogger(identity)
return PlatformLogger(identity ?: requester.simpleName ?: requester.qualifiedName)
}
}
})

View File

@ -1,575 +0,0 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file: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

@ -76,7 +76,7 @@ kotlin {
}
}
val mingwMain by getting {
val mingwX64Main by getting {
dependencies {
}
}

View File

@ -14,11 +14,11 @@ import io.ktor.utils.io.pool.*
/**
* 缓存 [ByteArray] 实例的 [ObjectPool]
*/
public object ByteArrayPool : DefaultPool<ByteArray>(256) {
public object ByteArrayPool : DefaultPool<ByteArray>(128) {
/**
* 每一个 [ByteArray] 的大小
*/
public const val BUFFER_SIZE: Int = 8192 * 8
public const val BUFFER_SIZE: Int = 4096
override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE)

View File

@ -121,8 +121,12 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len
}
}
public inline fun ByteArray.toReadPacket(offset: Int = 0, length: Int = this.size - offset): ByteReadPacket =
ByteReadPacket(this, offset = offset, length = length)
public inline fun ByteArray.toReadPacket(
offset: Int = 0,
length: Int = this.size - offset,
noinline release: (ByteArray) -> Unit = {}
): ByteReadPacket =
ByteReadPacket(this, offset = offset, length = length, block = release)
public inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
contract {

View File

@ -23,7 +23,7 @@ import kotlin.jvm.JvmName
*/
/**
* 255 -> 00 FF
* Converts a Short to its hex representation in network order (big-endian).
*/
public fun Short.toByteArray(): ByteArray = with(toInt()) {
byteArrayOf(
@ -33,7 +33,7 @@ public fun Short.toByteArray(): ByteArray = with(toInt()) {
}
/**
* 255 -> 00 00 00 FF
* Converts an Int to its hex representation in network order (big-endian).
*/
public fun Int.toByteArray(): ByteArray = byteArrayOf(
ushr(24).toByte(),
@ -43,7 +43,7 @@ public fun Int.toByteArray(): ByteArray = byteArrayOf(
)
/**
* 255 -> 00 00 00 FF
* Converts a Long to its hex representation in network order (big-endian).
*/
public fun Long.toByteArray(): ByteArray = byteArrayOf(
(ushr(56) and 0xFF).toByte(),
@ -56,10 +56,13 @@ public fun Long.toByteArray(): ByteArray = byteArrayOf(
(ushr(0) and 0xFF).toByte()
)
/**
* Converts an Int to its hex representation in network order (big-endian).
*/
public fun Int.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
/**
* 255 -> 00 FF
* Converts an UShort to its hex representation in network order (big-endian).
*/
public fun UShort.toByteArray(): ByteArray = with(toUInt()) {
byteArrayOf(
@ -68,22 +71,33 @@ public fun UShort.toByteArray(): ByteArray = with(toUInt()) {
)
}
/**
* Converts a Short to its hex representation in network order (big-endian).
*/
public fun Short.toUHexString(separator: String = " "): String = this.toUShort().toUHexString(separator)
/**
* Converts an UShort to its hex representation in network order (big-endian).
*/
public fun UShort.toUHexString(separator: String = " "): String =
this.toInt().shr(8).toUShort().toUByte().toUHexString() + separator + this.toUByte().toUHexString()
/**
* Converts an ULong to its hex representation in network order (big-endian).
*/
public fun ULong.toUHexString(separator: String = " "): String =
this.toLong().toUHexString(separator)
/**
* Converts a Long to its hex representation in network order (big-endian).
*/
public fun Long.toUHexString(separator: String = " "): String =
this.ushr(32).toUInt().toUHexString(separator) + separator + this.toUInt().toUHexString(separator)
/**
* 255 -> 00 FF
*/
public fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte())
/**
* Converts an UByte to its hex representation.
*/
public fun UByte.toUHexString(): String = this.toByte().toUHexString()
/**
@ -97,7 +111,7 @@ public fun UInt.toByteArray(): ByteArray = byteArrayOf(
)
/**
* [ByteArray] 后再转 hex
* Converts an UInt to its hex representation in network order (big-endian).
*/
public fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
@ -119,22 +133,23 @@ public fun UByte.fixToUHex(): String =
if (this.toInt() in 0..15) "0${this.toString(16).uppercase()}" else this.toString(16).uppercase()
/**
* [this] 4 [Byte] bits 合并为一个 [Int]
*
* 详细解释:
* 一个 [Byte] 8 bits
* 一个 [Int] 32 bits
* 本函数将 4 [Byte] bits 连接得到 [Int]
* Converts 4 bytes to an UInt in network order (big-endian).
*/
public fun ByteArray.toUInt(): UInt =
(this[0].toUInt().and(255u) shl 24) + (this[1].toUInt().and(255u) shl 16) + (this[2].toUInt()
.and(255u) shl 8) + (this[3].toUInt().and(
255u
) shl 0)
(this[0].toUInt().and(255u) shl 24)
.plus(this[1].toUInt().and(255u) shl 16)
.plus(this[2].toUInt().and(255u) shl 8)
.plus(this[3].toUInt().and(255u) shl 0)
/**
* Converts 2 bytes to an UShort in network order (big-endian).
*/
public fun ByteArray.toUShort(): UShort =
((this[0].toUInt().and(255u) shl 8) + (this[1].toUInt().and(255u) shl 0)).toUShort()
/**
* Converts 4 bytes to an Int in network order (big-endian).
*/
public fun ByteArray.toInt(offset: Int = 0): Int =
this[offset + 0].toInt().and(255).shl(24)
.plus(this[offset + 1].toInt().and(255).shl(16))

View File

@ -76,7 +76,7 @@ public fun MiraiFile.writeText(text: String) {
}
public fun MiraiFile.readText(): String {
return input().use { it.readText() }
return input().use { it.readAllText() }
}
public fun MiraiFile.readBytes(): ByteArray {

View File

@ -133,6 +133,8 @@ public fun Input._readTLVMap(
return map
}
public fun Input.readAllText(): String = Charsets.UTF_8.newDecoder().decode(this)
public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String =
String(this.readBytes(length), charset = charset) // stdlib

View File

@ -1,174 +0,0 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.utils
import io.ktor.utils.io.core.*
import kotlinx.cinterop.*
import platform.posix.PATH_MAX
import platform.posix.fopen
import platform.posix.getcwd
import platform.windows.*
internal actual class MiraiFileImpl actual constructor(
// canonical
path: String
) : MiraiFile {
override val path = path.replace("/", "\\")
actual companion object {
private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""")
private const val SEPARATOR = '\\'
@Suppress("UnnecessaryOptInAnnotation")
@OptIn(UnsafeNumber::class)
actual fun getWorkingDir(): MiraiFile {
val path = memScoped {
ByteArray(PATH_MAX).usePinned {
getcwd(it.addressOf(0), it.get().size.convert())
it.get().toKString()
}
}
return MiraiFile.create(path)
}
}
override val absolutePath: String = kotlin.run {
val result = ROOT_REGEX.matchEntire(path) ?: return@run path.dropLastWhile { it.isSeparator() }
return@run result.groups.first()!!.value
}
private fun Char.isSeparator() = this == '/' || this == '\\'
override val parent: MiraiFile? by lazy {
val absolute = absolutePath
val p = absolute.substringBeforeLast(SEPARATOR, "")
if (p.isEmpty()) {
return@lazy null
}
if (p.lastOrNull() == ':') {
if (absolute.lastIndexOf(SEPARATOR) == p.lastIndex) {
// file is C:/
return@lazy null
} else {
return@lazy MiraiFileImpl("$p/") // file is C:/xxx
}
}
MiraiFileImpl(p)
}
override val name: String
get() = if (absolutePath.matches(ROOT_REGEX)) absolutePath
else absolutePath.substringAfterLast('/')
init {
checkName(absolutePath.substringAfterLast('/')) // do not check drive letter
}
private fun checkName(name: String) {
name.substringAfterLast('/').forEach { c ->
if (c in """\/:?*"><|""") {
throw IllegalArgumentException("'${name}' contains illegal character '$c'.")
}
}
memScoped {
val b = alloc<WINBOOLVar>()
CheckNameLegalDOS8Dot3A(absolutePath, nullPtr(), 0, nullPtr(), b.ptr)
if (b.value != 1) {
throw IllegalArgumentException("'${name}' contains illegal character.")
}
}
}
override val length: Long
get() = useStat { it.st_size.convert() } ?: 0
override val isFile: Boolean
get() = getFileAttributes() flag FILE_ATTRIBUTE_NORMAL
override val isDirectory: Boolean
get() = getFileAttributes() flag FILE_ATTRIBUTE_DIRECTORY
override fun exists(): Boolean = getFileAttributes() != INVALID_FILE_ATTRIBUTES
private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesA(absolutePath) }
override fun resolve(path: String): MiraiFile {
when (path) {
"." -> return this
".." -> return parent ?: this // root
}
if (ROOT_REGEX.find(path) != null) { // absolute
return MiraiFileImpl(path)
}
return MiraiFileImpl(this.absolutePath + SEPARATOR + path) // assuming path is 'appendable'
}
override fun resolve(file: MiraiFile): MiraiFile {
val parent = file.parent ?: return resolve(file.name)
return resolve(parent).resolve(file.name)
}
override fun createNewFile(): Boolean {
memScoped {
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
val handle = CreateFileA(
absolutePath,
GENERIC_READ,
FILE_SHARE_WRITE,
nullPtr(),
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
nullPtr()
)
if (handle == NULL) return false
CloseHandle(handle)
return true
}
}
override fun delete(): Boolean {
return if (isFile) {
DeleteFileA(absolutePath) == 0
} else {
RemoveDirectoryA(absolutePath) == 0
}
}
override fun mkdir(): Boolean {
memScoped {
val v = alloc<_SECURITY_ATTRIBUTES>()
return CreateDirectoryA(absolutePath, v.ptr) == 0
}
}
override fun mkdirs(): Boolean {
if (this.parent?.mkdirs() == false) {
return false
}
return mkdir()
}
override fun input(): Input {
val handle = fopen(absolutePath, "r")
if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'")
return PosixInputForFile(handle!!)
}
override fun output(): Output {
val handle = fopen(absolutePath, "w")
if (handle == NULL) throw IllegalStateException("Failed to open file '$absolutePath'")
return PosixFileInstanceOutput(handle!!)
}
}

View File

@ -1,35 +0,0 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.utils
import kotlin.math.absoluteValue
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
private val rand = Random.nextInt().absoluteValue
override val baseTempDir: MiraiFile = MiraiFile.create("mirai_unit_tests")
override val tempPath = "mirai_unit_tests/temp$rand"
@Test
override fun parent() {
assertEquals("C:/Users/Shared/mirai_test", tempDir.parent!!.absolutePath)
super.parent()
}
@Test
override fun `resolve absolute`() {
MiraiFile.create("$tempPath/").resolve("C:/Users").let {
assertEquals("C:/Users", it.path)
assertEquals("C:/Users", it.absolutePath)
}
}
}

View File

@ -0,0 +1,343 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.utils
import io.ktor.utils.io.bits.*
import io.ktor.utils.io.core.*
import io.ktor.utils.io.errors.*
import kotlinx.cinterop.*
import platform.posix.*
import platform.windows.*
private fun getFullPathName(path: String): String = memScoped {
ShortArray(MAX_PATH).usePinned { pin ->
val len = GetFullPathNameW(path, MAX_PATH, pin.addressOf(0).reinterpret(), null).toInt()
if (len != 0) {
return pin.get().toKStringFromUtf16(len)
} else {
when (val errno = errno) {
ENOTDIR -> return@memScoped path
EACCES -> return@memScoped path // permission denied
ENOENT -> return@memScoped path // no such file
else -> throw IllegalArgumentException(
"Invalid path($errno): $path",
cause = PosixException.forErrno(posixFunctionName = "GetFullPathNameW()")
)
}
}
}
}
private fun ShortArray.toKStringFromUtf16(len: Int): String {
val chars = CharArray(len)
var index = 0
while (index < len) {
chars[index] = this[index].toInt().toChar()
++index
}
return chars.concatToString()
}
internal actual class MiraiFileImpl actual constructor(
// canonical
path: String
) : MiraiFile {
override val path = path.replace("/", "\\")
actual companion object {
private val ROOT_REGEX = Regex("""^([a-zA-z]+:[/\\])""")
private const val SEPARATOR = '\\'
@Suppress("UnnecessaryOptInAnnotation")
@OptIn(UnsafeNumber::class)
actual fun getWorkingDir(): MiraiFile {
val path = memScoped {
ByteArray(PATH_MAX).usePinned {
getcwd(it.addressOf(0), it.get().size.convert())
it.get().toKString()
}
}
return MiraiFile.create(path)
}
}
override val absolutePath: String by lazy {
val result = ROOT_REGEX.matchEntire(this.path)
?: return@lazy getFullPathName(this.path).removeSuffix(SEPARATOR.toString())
return@lazy result.groups.first()!!.value
}
private fun Char.isSeparator() = this == '/' || this == '\\'
override val parent: MiraiFile? by lazy {
if (ROOT_REGEX.matchEntire(this.path) != null) return@lazy null
val absolute = absolutePath
val p = absolute.substringBeforeLast(SEPARATOR, "")
if (p.isEmpty()) {
return@lazy null
}
if (p.lastOrNull() == ':') {
if (absolute.lastIndexOf(SEPARATOR) == p.lastIndex) {
// file is C:/
return@lazy null
} else {
return@lazy MiraiFileImpl("$p/") // file is C:/xxx
}
}
MiraiFileImpl(p)
}
override val name: String
get() = if (absolutePath.matches(ROOT_REGEX)) absolutePath
else absolutePath.substringAfterLast(SEPARATOR)
init {
checkName(absolutePath.substringAfterLast(SEPARATOR)) // do not check drive letter
}
private fun checkName(name: String) {
name.substringAfterLast(SEPARATOR).forEach { c ->
if (c in """\/:?*"><|""") {
throw IllegalArgumentException("'${name}' contains illegal character '$c'.")
}
}
// memScoped {
// val b = alloc<WINBOOLVar>()
// CheckNameLegalDOS8Dot3A(absolutePath, nullPtr(), 0, nullPtr(), b.ptr)
// if (b.value != 1) {
// throw IllegalArgumentException("'${name}' contains illegal character.")
// }
// }
}
override val length: Long
get() = useStat { it.st_size.convert() } ?: 0
// memScoped {
// val handle = CreateFileW(
// absolutePath,
// GENERIC_READ,
// FILE_SHARE_READ,
// null,
// OPEN_EXISTING,
// FILE_ATTRIBUTE_NORMAL,
// null
// ) ?: return@memScoped 0
// val length = alloc<DWORDVar>()
// if (GetFileSize(handle, length.ptr) == INVALID_FILE_SIZE) {
// if (GetLastError() == NO_ERROR.toUInt()) {
// return INVALID_FILE_SIZE.convert()
// }
// throw PosixException.forErrno(posixFunctionName = "GetFileSize()").wrapIO()
// }
// if (CloseHandle(handle) == FALSE) {
// throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO()
// }
// length.value.convert()
// }
override val isFile: Boolean
get() = useStat { it.st_mode.convert<UInt>() flag S_IFREG } ?: false
override val isDirectory: Boolean
get() = useStat { it.st_mode.convert<UInt>() flag S_IFDIR } ?: false
override fun exists(): Boolean = getFileAttributes() != INVALID_FILE_ATTRIBUTES
private fun getFileAttributes(): DWORD = memScoped { GetFileAttributesW(absolutePath) }
override fun resolve(path: String): MiraiFile {
when (path) {
"." -> return this
".." -> return parent ?: this // root
}
if (ROOT_REGEX.find(path) != null) { // absolute
return MiraiFileImpl(path)
}
return MiraiFileImpl(this.absolutePath + SEPARATOR + path) // assuming path is 'appendable'
}
override fun resolve(file: MiraiFile): MiraiFile {
val parent = file.parent ?: return resolve(file.name)
return resolve(parent).resolve(file.name)
}
override fun createNewFile(): Boolean {
// https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
val handle = CreateFileW(
absolutePath,
GENERIC_READ,
FILE_SHARE_DELETE,
null,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL,
null
)
if (handle == null || handle == INVALID_HANDLE_VALUE) {
return false
}
if (CloseHandle(handle) == FALSE) {
throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO()
}
return true
}
override fun delete(): Boolean {
return if (isFile) {
DeleteFileW(absolutePath) != 0
} else {
RemoveDirectoryW(absolutePath) != 0
}
}
override fun mkdir(): Boolean {
memScoped {
val v = alloc<_SECURITY_ATTRIBUTES>()
return CreateDirectoryW(absolutePath, v.ptr) != 0
}
}
override fun mkdirs(): Boolean {
this.parent?.mkdirs()
return mkdir()
}
override fun input(): Input {
// println(absolutePath)
// val handle2 = fopen(absolutePath, "rb") ?:throw IOException(
// "Failed to open file '$absolutePath'",
// PosixException.forErrno(posixFunctionName = "fopen()")
// )
// return PosixInputForFile(handle2)
// Will get I/O operation failed due to posix error code 2
val handle = CreateFileW(
absolutePath,
GENERIC_READ,
FILE_SHARE_DELETE,
null,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
null
)
if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException(
"Failed to open file '$absolutePath'",
PosixException.forErrno(posixFunctionName = "CreateFileW()")
)
return WindowsFileInput(handle)
}
override fun output(): Output {
// val handle2 = fopen(absolutePath, "wb")
// ?: throw IOException(
// "Failed to open file '$absolutePath'",
// PosixException.forErrno(posixFunctionName = "fopen()")
// )
// return PosixFileInstanceOutput(handle)
//
val handle = CreateFileW(
absolutePath,
GENERIC_WRITE,
FILE_SHARE_DELETE,
null,
(if (exists()) TRUNCATE_EXISTING else CREATE_NEW).toUInt(),
FILE_ATTRIBUTE_NORMAL,
null
)
if (handle == null || handle == INVALID_HANDLE_VALUE) throw IOException(
"Failed to open file '$absolutePath'",
PosixException.forErrno(posixFunctionName = "CreateFileW()")
)
return WindowsFileOutput(handle)
}
override fun hashCode(): Int {
return this.path.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (!isSameType(this, other)) return false
return this.path == other.path
}
override fun toString(): String {
return "MiraiFileImpl($path)"
}
}
internal class WindowsFileInput(private val file: HANDLE) : Input() {
private var closed = false
override fun fill(destination: Memory, offset: Int, length: Int): Int {
if (file == INVALID_HANDLE_VALUE) return 0
memScoped {
val n = alloc<DWORDVar>()
if (ReadFile(file, destination.pointer + offset, length.convert(), n.ptr, null) == FALSE) {
throw PosixException.forErrno(posixFunctionName = "ReadFile()").wrapIO()
}
return n.value.convert<UInt>().toInt()
}
}
override fun closeSource() {
if (closed) return
closed = true
if (file != INVALID_HANDLE_VALUE) {
if (CloseHandle(file) == FALSE) {
throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO()
}
}
}
}
@Suppress("DEPRECATION")
internal class WindowsFileOutput(private val file: HANDLE) : Output() {
private var closed = false
override fun flush(source: Memory, offset: Int, length: Int) {
val end = offset + length
var currentOffset = offset
memScoped {
val written = alloc<UIntVar>()
while (currentOffset < end) {
val result = WriteFile(
file,
source.pointer + currentOffset.convert(),
(end - currentOffset).convert(),
written.ptr,
null
).convert<Int>()
if (result == FALSE) {
throw PosixException.forErrno(posixFunctionName = "WriteFile()").wrapIO()
}
currentOffset += written.value.toInt()
}
}
}
override fun closeDestination() {
if (closed) return
closed = true
if (CloseHandle(file) == FALSE) {
throw PosixException.forErrno(posixFunctionName = "CloseHandle()").wrapIO()
}
}
}

View File

@ -11,4 +11,6 @@ package net.mamoe.mirai.utils
import platform.windows.GetCurrentProcessorNumber
public actual fun availableProcessors(): Int = GetCurrentProcessorNumber().toInt()
public actual fun availableProcessors(): Int =
GetCurrentProcessorNumber().toInt().coerceAtLeast(4) // somehow it worked on my machine but not on CI

View File

@ -0,0 +1,45 @@
/*
* 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.math.absoluteValue
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
internal class WindowsMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
private val rand = Random.nextInt().absoluteValue
override val baseTempDir: MiraiFile = MiraiFile.create("C:\\mirai_unit_tests")
override val tempPath = "C:\\mirai_unit_tests\\temp$rand"
@Test
override fun parent() {
assertEquals("C:\\mirai_unit_tests", tempDir.parent!!.absolutePath)
super.parent()
}
override fun `canonical paths for non-canonical input`() {
super.`canonical paths for non-canonical input`()
// extra /sss/..
MiraiFile.create("$tempPath/sss/..").resolve("test").let {
assertPathEquals("${tempPath}/test", it.path) // Windows resolves always
assertPathEquals("${tempPath}/test", it.absolutePath)
}
}
@Test
override fun `resolve absolute`() {
MiraiFile.create("$tempPath/").resolve("C:\\mirai_unit_tests").let {
assertEquals("C:\\mirai_unit_tests", it.path)
assertEquals("C:\\mirai_unit_tests", it.absolutePath)
}
}
}

View File

@ -12,5 +12,7 @@
package net.mamoe.mirai.utils
public actual inline fun measureTimeMillis(block: () -> Unit): Long {
return kotlin.system.measureTimeMillis(block)
val start = currentTimeMillis() // getTimeMillis in stdlib doesn't work on some native targets.
block()
return currentTimeMillis() - start
}

View File

@ -10,10 +10,13 @@
package net.mamoe.mirai.utils
internal actual fun hash(e: Throwable): Long {
// Stacktrace analysis not available
var hashCode = 1L
for (stackTraceAddress in e.getStackTraceAddresses()) {
val trace = e.getStackTraceAddresses()
for (stackTraceAddress in trace) {
hashCode = (hashCode xor stackTraceAddress).shl(1)
}
return hashCode
// Somehow stacktrace analysis is on my own Windows machine but not on GitHub Actions.
// Hashing with a class to tentatively not filter out different types.
return hashCode xor e::class.hashCode().toLongUnsigned()
}

View File

@ -215,5 +215,5 @@ internal class PosixInputForFile(val file: CPointer<FILE>) : AbstractInput() {
}
@OptIn(ExperimentalIoApi::class)
internal fun PosixException.wrapIO(): IOException =
public fun PosixException.wrapIO(): IOException =
IOException("I/O operation failed due to posix error code $errno", this)

View File

@ -38,12 +38,18 @@ public object Services {
}
}
public fun implementations(baseClass: String): List<Any>? {
public fun implementations(baseClass: String): List<Lazy<Any>>? {
lock.withLock {
return registered[baseClass]?.map { it.instance }
}
}
public fun print(): String {
lock.withLock {
return registered.entries.joinToString { "${it.key}:${it.value}" }
}
}
}
@Suppress("UNCHECKED_CAST")
@ -57,10 +63,10 @@ public actual fun <T : Any> loadService(
clazz: KClass<out T>,
fallbackImplementation: String?
): T = loadServiceOrNull(clazz, fallbackImplementation)
?: error("Could not load service '${clazz.qualifiedName ?: clazz}'")
?: error("Could not load service '${clazz.qualifiedName ?: clazz}'. Current services: ${Services.print()}")
public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> =
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence().orEmpty().castUp()
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp()
private fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>) =
clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")

View File

@ -11,6 +11,8 @@
package net.mamoe.mirai.utils
import kotlinx.atomicfu.locks.ReentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlinx.cinterop.*
import platform.posix.*
@ -20,23 +22,28 @@ import platform.posix.*
public actual fun currentTimeMillis(): Long {
// Do not use getTimeMillis from stdlib, it doesn't support iosSimulatorArm64
memScoped {
val timeT = alloc<time_tVar>()
time(timeT.ptr)
return timeT.value.toLongUnsigned()
val spec = alloc<timespec>()
clock_gettime(CLOCK_REALTIME.convert(), spec.ptr)
return (spec.tv_sec * 1000 + spec.tv_nsec / 1e6).toLong()
}
}
public actual fun currentTimeFormatted(format: String?): String {
private val timeLock = ReentrantLock()
@OptIn(UnsafeNumber::class)
public actual fun currentTimeFormatted(format: String?): String = timeLock.withLock {
memScoped {
val timeT = alloc<time_tVar>()
time(timeT.ptr)
val tm = localtime(timeT.ptr)
try {
val bb = allocArray<ByteVar>(40)
strftime(bb, 40, "%Y-%M-%d %H:%M:%S", tm);
return bb.toKString()
} finally {
free(tm)
}
// http://www.cplusplus.com/reference/clibrary/ctime/localtime/
// tm returns a static pointer which doesn't need to free
val tm = localtime(timeT.ptr) // localtime is not thread-safe
val bb = allocArray<ByteVar>(40)
strftime(bb, 40, "%Y-%m-%d %H:%M:%S", tm);
bb.toKString()
}
}

View File

@ -9,10 +9,12 @@
package net.mamoe.mirai.utils
private val properties: MutableMap<String, String> = ConcurrentHashMap()
internal actual fun getProperty(name: String, default: String): String? {
TODO("Not yet implemented")
return properties.getOrElse(name) { default }
}
internal actual fun setProperty(name: String, value: String) {
TODO("Not yet implemented")
properties[name] = value
}

View File

@ -24,7 +24,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
@AfterTest
fun afterTest() {
println("Cleaning up...")
baseTempDir.deleteRecursively()
println("deleteRecursively:" + baseTempDir.deleteRecursively())
}
@BeforeTest
@ -35,8 +35,8 @@ internal abstract class AbstractNativeMiraiFileImplTest {
@Test
fun `canonical paths for canonical input`() {
assertEquals(tempPath, tempDir.path)
assertEquals(tempPath, tempDir.absolutePath)
assertPathEquals(tempPath, tempDir.path)
assertPathEquals(tempPath, tempDir.absolutePath)
}
@Test
@ -46,31 +46,26 @@ internal abstract class AbstractNativeMiraiFileImplTest {
}
@Test
fun `canonical paths for non-canonical input`() {
open fun `canonical paths for non-canonical input`() {
// extra /
MiraiFile.create("$tempPath/").resolve("test").let {
assertEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath)
assertPathEquals("${tempPath}/test", it.path)
assertPathEquals("${tempPath}/test", it.absolutePath)
}
// extra //
MiraiFile.create("$tempPath//").resolve("test").let {
assertEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath)
assertPathEquals("${tempPath}/test", it.path)
assertPathEquals("${tempPath}/test", it.absolutePath)
}
// extra /.
MiraiFile.create("$tempPath/.").resolve("test").let {
assertEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath)
assertPathEquals("${tempPath}/test", it.path)
assertPathEquals("${tempPath}/test", it.absolutePath)
}
// extra /./.
MiraiFile.create("$tempPath/./.").resolve("test").let {
assertEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath)
}
// extra /sss/..
MiraiFile.create("$tempPath/sss/..").resolve("test").let {
assertEquals("${tempPath}/sss/../test", it.path) // because file is not found
assertEquals("${tempPath}/sss/../test", it.absolutePath)
assertPathEquals("${tempPath}/test", it.path)
assertPathEquals("${tempPath}/test", it.absolutePath)
}
}
@ -90,7 +85,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
assertFalse { tempDir.resolve("not_existing_dir").exists() }
assertEquals(0L, tempDir.resolve("not_existing_dir").length)
assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
assertNotEquals(0L, tempDir.resolve("not_existing_dir").length)
// assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) // length is platform-dependent, on Windows it is 0 but on unix it is not
assertTrue { tempDir.resolve("not_existing_dir").exists() }
}
@ -98,20 +93,27 @@ internal abstract class AbstractNativeMiraiFileImplTest {
fun `isFile isDirectory`() {
assertTrue { tempDir.exists() }
println("1")
assertFalse { tempDir.resolve("not_existing_file.txt").exists() }
assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile)
println("1")
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() }
assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile)
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_file.txt").exists() }
println("1")
assertFalse { tempDir.resolve("not_existing_dir").exists() }
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_dir").exists() }
}
@ -134,7 +136,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
@Test
fun readText() {
tempDir.resolve("readText1.txt").let { file ->
tempDir.resolve("readText2.txt").let { file ->
assertTrue { !file.exists() }
assertFailsWith<IOException> { file.readText() }
@ -143,4 +145,39 @@ internal abstract class AbstractNativeMiraiFileImplTest {
assertEquals(text, file.readText())
}
}
private val bigText = "some text".repeat(10000)
@Test
fun writeBigText() {
// new file
tempDir.resolve("writeText3.txt").let { file ->
file.writeText(bigText)
assertEquals(bigText.length, file.length.toInt())
}
// override
tempDir.resolve("writeText4.txt").let { file ->
file.writeText(bigText)
assertEquals(bigText.length, file.length.toInt())
}
}
@Test
fun readBigText() {
tempDir.resolve("readText4.txt").let { file ->
assertTrue { !file.exists() }
assertFailsWith<IOException> { file.readText() }
file.writeText(bigText)
println("reading text")
val read = file.readText()
assertEquals(bigText.length, read.length)
assertEquals(bigText, read)
}
}
protected fun assertPathEquals(expected: String, actual: String, message: String? = null) {
asserter.assertEquals(message, expected.replace("\\", "/"), actual.replace("\\", "/"))
}
}

View File

@ -17,7 +17,7 @@ internal class TimeUtilsTest {
@Test
fun `can get currentTimeMillis`() {
val time = currentTimeMillis()
assertTrue(time.toString()) { time > 1642549113 }
assertTrue(time.toString()) { time > 1654209523269 }
}
@Test

View File

@ -157,7 +157,7 @@ internal actual class MiraiFileImpl actual constructor(
@OptIn(ExperimentalIoApi::class)
override fun input(): Input {
val handle = fopen(absolutePath, "r")
val handle = fopen(absolutePath, "rb")
?: throw IOException(
"Failed to open file '$absolutePath'",
PosixException.forErrno(posixFunctionName = "fopen()")
@ -167,7 +167,7 @@ internal actual class MiraiFileImpl actual constructor(
@OptIn(ExperimentalIoApi::class)
override fun output(): Output {
val handle = fopen(absolutePath, "w")
val handle = fopen(absolutePath, "wb")
?: throw IOException(
"Failed to open file '$absolutePath'",
PosixException.forErrno(posixFunctionName = "fopen()")

View File

@ -28,6 +28,16 @@ internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
super.parent()
}
override fun `canonical paths for non-canonical input`() {
super.`canonical paths for non-canonical input`()
// extra /sss/..
MiraiFile.create("$tempPath/sss/..").resolve("test").let {
assertPathEquals("${tempPath}/sss/../test", it.path) // because file is not found
assertPathEquals("${tempPath}/sss/../test", it.absolutePath)
}
}
@Test
override fun `resolve absolute`() {
MiraiFile.create("$tempPath/").resolve("/Users").let {

View File

@ -1 +1,2 @@
src/jvmTest/kotlin/local
src/jvmTest/kotlin/local
test-sandbox/

View File

@ -10,6 +10,7 @@
@file:Suppress("UNUSED_VARIABLE")
import BinaryCompatibilityConfigurator.configureBinaryValidators
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
@ -57,6 +58,8 @@ kotlin {
implementation(bouncycastle)
implementation(`log4j-api`)
implementation(`netty-all`)
implementation(`ktor-client-okhttp`)
api(`kotlinx-coroutines-core`)
}
}
@ -102,6 +105,55 @@ kotlin {
dependencies {
}
}
NATIVE_TARGETS.forEach { target ->
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("OpenSSL")
.apply {
defFile = projectDir.resolve("src/nativeMain/cinterop/OpenSSL.def")
packageName("openssl")
}
}
UNIX_LIKE_TARGETS.forEach { target ->
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket")
.apply {
defFile = projectDir.resolve("src/unixMain/cinterop/Socket.def")
packageName("sockets")
}
}
WIN_TARGETS.forEach { target ->
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket")
.apply {
defFile = projectDir.resolve("src/mingwX64Main/cinterop/Socket.def")
packageName("sockets")
}
}
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-curl`)
}
}
configure(LINUX_TARGETS.map { getByName(it + "Main") }) {
dependencies {
implementation(`ktor-client-curl`)
}
}
val darwinMain by getting {
dependencies {
implementation(`ktor-client-ios`)
}
}
disableCrossCompile()
// val unixMain by getting {
// dependencies {
// implementation(`ktor-client-cio`)
// }
// }
}
}

View File

@ -0,0 +1,127 @@
/*
* 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("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
package android.util
import net.mamoe.mirai.internal.utils.StdoutLogger
// Dummy implementation for tests, since we don't have an Android SDK
@Suppress("UNUSED_PARAMETER", "unused")
object Log {
const val VERBOSE = 2
const val DEBUG = 3
const val INFO = 4
const val WARN = 5
const val ERROR = 6
const val ASSERT = 7
private val stdout = StdoutLogger("AndroidLog")
@JvmStatic
fun v(tag: String?, msg: String?): Int {
stdout.verbose(msg)
return 0
}
@JvmStatic
fun v(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.verbose(msg, tr)
return 0
}
@JvmStatic
fun d(tag: String?, msg: String?): Int {
stdout.debug(msg, tr)
return 0
}
@JvmStatic
fun d(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.debug(msg, tr)
return 0
}
@JvmStatic
fun i(tag: String?, msg: String?): Int {
stdout.info(msg, tr)
return 0
}
@JvmStatic
fun i(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.info(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, msg: String?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun w(tag: String?, tr: Throwable?): Int {
stdout.warning(msg, tr)
return 0
}
@JvmStatic
fun e(tag: String?, msg: String?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun e(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, msg: String?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun wtf(tag: String?, msg: String?, tr: Throwable?): Int {
stdout.error(msg, tr)
return 0
}
@JvmStatic
fun getStackTraceString(tr: Throwable): String {
return tr.stackTraceToString()
}
@JvmStatic
fun println(priority: Int, tag: String?, msg: String?): Int {
stdout.info(msg, tr)
return 0
}
private inline val tr get() = null
private inline val msg get() = null
}

View File

@ -7,9 +7,12 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmName("MiraiImplKt_common")
package net.mamoe.mirai.internal
import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
@ -31,25 +34,14 @@ import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl.Companion.impl
import net.mamoe.mirai.internal.event.EventChannelToEventDispatcherAdapter
import net.mamoe.mirai.internal.event.InternalEventMechanism
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
import net.mamoe.mirai.internal.message.EmptyRefineContext
import net.mamoe.mirai.internal.message.RefineContext
import net.mamoe.mirai.internal.message.SimpleRefineContext
import net.mamoe.mirai.internal.message.data.*
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl
import net.mamoe.mirai.internal.message.image.*
import net.mamoe.mirai.internal.message.image.OfflineGroupImage
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
import net.mamoe.mirai.internal.message.source.*
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromFriendImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromGroupImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromStrangerImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromTempImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToStrangerImpl
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToTempImpl
import net.mamoe.mirai.internal.message.toMessageChainNoSource
import net.mamoe.mirai.internal.network.components.EventDispatcher
import net.mamoe.mirai.internal.network.highway.ChannelKind
import net.mamoe.mirai.internal.network.highway.ResourceKind
@ -77,14 +69,21 @@ import net.mamoe.mirai.message.MessageSerializers
import net.mamoe.mirai.message.action.Nudge
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.*
import kotlin.jvm.JvmName
internal fun getMiraiImpl() = Mirai as MiraiImpl
internal expect fun createDefaultHttpClient(): HttpClient
@Suppress("FunctionName")
internal expect fun _MiraiImpl_static_init()
@OptIn(LowLevelApi::class)
// not object for ServiceLoader.
internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
companion object {
init {
_MiraiImpl_static_init()
MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())
MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())
MessageSerializers.registerSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer())
@ -156,13 +155,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
@Deprecated("Mirai is not going to use ktor. This is deprecated for removal.", level = DeprecationLevel.WARNING)
override var Http: HttpClient = HttpClient() {
install(HttpTimeout) {
this.requestTimeoutMillis = 30_0000
this.connectTimeoutMillis = 30_0000
this.socketTimeoutMillis = 30_0000
}
}
override var Http: HttpClient = createDefaultHttpClient()
override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@ -870,17 +863,21 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
return main.toForwardMessageNodes(bot, context)
}
protected open suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node {
private suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node {
val msg = this
@Suppress("USELESS_CAST") // compiler bug, do not remove
val senderName = (msg.msgHead.groupInfo?.groupCard
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
?: msg.msgHead.fromUin.toString()) as String
val chain = listOf(msg)
.toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
.refineDeep(bot, refineContext)
return ForwardMessage.Node(
senderId = msg.msgHead.fromUin,
time = msg.msgHead.msgTime,
senderName = msg.msgHead.groupInfo?.groupCard
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
?: msg.msgHead.fromUin.toString(),
messageChain = listOf(msg)
.toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
.refineDeep(bot, refineContext)
senderName = senderName,
messageChain = chain
)
}

View File

@ -52,7 +52,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
import net.mamoe.mirai.internal.utils.ImagePatcher
import net.mamoe.mirai.internal.utils.RemoteFileImpl
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.subLogger
import net.mamoe.mirai.message.MessageReceipt
@ -117,8 +116,18 @@ private val logger by lazy {
internal fun Bot.nickIn(context: Contact): String =
if (context is Group) context.botAsMember.nameCardOrNick else bot.nick
internal expect class GroupImpl constructor(
bot: QQAndroidBot,
parentCoroutineContext: CoroutineContext,
id: Long,
groupInfo: GroupInfo,
members: ContactList<NormalMemberImpl>,
) : Group, CommonGroupImpl {
companion object
}
@Suppress("PropertyName")
internal class GroupImpl constructor(
internal abstract class CommonGroupImpl constructor(
bot: QQAndroidBot,
parentCoroutineContext: CoroutineContext,
override val id: Long,
@ -128,31 +137,27 @@ internal class GroupImpl constructor(
companion object
val uin: Long = groupInfo.uin
override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo)
override var name: String by settings::name
final override val settings: GroupSettingsImpl = GroupSettingsImpl(this.cast(), groupInfo)
final override var name: String by settings::name
override lateinit var owner: NormalMemberImpl
override lateinit var botAsMember: NormalMemberImpl
final override lateinit var owner: NormalMemberImpl
final override lateinit var botAsMember: NormalMemberImpl
internal val botAsMemberInitialized get() = ::botAsMember.isInitialized
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.WARNING)
@DeprecatedSinceMirai(warningSince = "2.8")
override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
final override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
val lastTalkative = atomic<NormalMemberImpl?>(null)
override val announcements: Announcements by lazy {
final override val announcements: Announcements by lazy {
AnnouncementsImpl(
this,
this as GroupImpl,
bot.network.logger.subLogger("Group $id")
)
}
val groupPkgMsgParsingCache = GroupPkgMsgParsingCache()
private val messageProtocolStrategy: MessageProtocolStrategy<GroupImpl> = GroupMessageProtocolStrategy(this)
private val messageProtocolStrategy: MessageProtocolStrategy<GroupImpl> = GroupMessageProtocolStrategy(this.cast())
override suspend fun quit(): Boolean {
check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
@ -162,11 +167,11 @@ internal class GroupImpl constructor(
}
val response: ProfileService.GroupMngReq.GroupMngReqResponse = bot.network.sendAndExpect(
ProfileService.GroupMngReq(bot.client, this@GroupImpl.id), 5000, 2
ProfileService.GroupMngReq(bot.client, this@CommonGroupImpl.id), 5000, 2
)
check(response.errorCode == 0) {
"Group.quit failed: $response".also {
bot.groups.delegate.add(this@GroupImpl)
bot.groups.delegate.add(this@CommonGroupImpl.castUp())
}
}
BotLeaveEvent.Active(this).broadcast()
@ -186,7 +191,7 @@ internal class GroupImpl constructor(
check(!isBotMuted) { throw BotIsBeingMutedException(this, message) }
return sendMessageImpl(
message,
messageProtocolStrategy,
messageProtocolStrategy.castUp(),
::GroupMessagePreSendEvent,
::GroupMessagePostSendEvent.cast()
)
@ -220,7 +225,8 @@ internal class GroupImpl constructor(
when (response) {
is ImgStore.GroupPicUp.Response.Failed -> {
ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
ImageUploadEvent.Failed(this@CommonGroupImpl, resource, response.resultCode, response.message)
.broadcast()
if (response.message == "over file size max") throw OverFileSizeMaxException()
error("upload group image failed with reason ${response.message}")
}
@ -239,7 +245,7 @@ internal class GroupImpl constructor(
it.fileId = response.fileId.toInt()
}
.also { it.putIntoCache() }
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
.also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() }
}
is ImgStore.GroupPicUp.Response.RequireUpload -> {
// val servers = response.uploadIpList.zip(response.uploadPortList)
@ -268,7 +274,7 @@ internal class GroupImpl constructor(
)
}.also { it.fileId = response.fileId.toInt() }
.also { it.putIntoCache() }
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
.also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() }
}
}
}
@ -350,7 +356,7 @@ internal class GroupImpl constructor(
val result = bot.network.sendAndExpect(
TroopEssenceMsgManager.SetEssence(
bot.client,
this@GroupImpl.uin,
this@CommonGroupImpl.uin,
source.internalIds.first(),
source.ids.first()
), 5000, 2

View File

@ -30,6 +30,7 @@ internal abstract class MessageProtocol(
}
fun collectProcessors(processorCollector: ProcessorCollector) {
println("collectProcessors, this=$this, class=${this::class}")
processorCollector.collectProcessorsImpl()
}

View File

@ -10,6 +10,7 @@
package net.mamoe.mirai.internal.network.components
import io.ktor.client.request.*
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -22,6 +23,7 @@ import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.currentTimeSeconds
import kotlin.time.Duration.Companion.seconds
/**
@ -88,7 +90,7 @@ internal class EcdhInitialPublicKeyUpdaterImpl(
logger.info("ECDH key is invalid, start to fetch ecdh public key from server.")
val respStr =
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
Mirai.Http.get<String>("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}")
withTimeout(10.seconds) { Mirai.Http.get<String>("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}") }
val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr)
resp.pubKeyMeta.let { meta ->
val isValid = ECDH.verifyPublicKey(

View File

@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.components.ServerList.Companion.DEFAULT_SERVER_LIST
import net.mamoe.mirai.internal.network.handler.SocketAddress
import net.mamoe.mirai.internal.network.handler.createSocketAddress
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.TestOnly
import net.mamoe.mirai.utils.info
@ -33,7 +34,7 @@ internal data class ServerAddress(
return "$host:$port"
}
fun toSocketAddress(): SocketAddress = SocketAddress(host, port)
fun toSocketAddress(): SocketAddress = createSocketAddress(host, port)
}
/**

View File

@ -11,6 +11,8 @@ package net.mamoe.mirai.internal.network.handler
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.onFailure
import net.mamoe.mirai.internal.network.components.*
import net.mamoe.mirai.internal.network.handler.selector.NetworkException
import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector
@ -89,14 +91,67 @@ internal abstract class CommonNetworkHandler<Conn>(
internal inner class PacketDecodePipeline(parentContext: CoroutineContext) :
CoroutineScope by parentContext.childScope() {
private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] }
fun send(raw: RawIncomingPacket) {
private val queue: Channel<ByteReadPacket> = Channel<ByteReadPacket>(Channel.BUFFERED) { undelivered ->
launch { sendQueue(undelivered) }
}.also { channel -> coroutineContext[Job]!!.invokeOnCompletion { channel.close(it) } }
private suspend inline fun sendQueue(packet: ByteReadPacket) {
queue.send(packet)
}
init {
launch {
packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" }
val result = packetCodec.processBody(context.bot, raw)
if (result == null) {
collectUnknownPacket(raw)
} else collectReceived(result)
while (isActive) {
val result = queue.receiveCatching()
packetLogger.verbose { "Decoding packet: $result" }
result.onFailure { if (it is CancellationException) return@launch }
result.getOrNull()?.let { packet ->
try {
val decoded = decodePacket(packet)
processBody(decoded)
} catch (e: Throwable) {
if (e is CancellationException) return@launch
handleExceptionInDecoding(e)
logger.error("Error while decoding packet '${packet}'", e)
}
}
}
}
}
private fun decodePacket(packet: ByteReadPacket): RawIncomingPacket {
return if (packetLogger.isDebugEnabled) {
val bytes = packet.readBytes()
logger.verbose { "Decoding: len=${bytes.size}, value=${bytes.toUHexString()}" }
val raw = packetCodec.decodeRaw(
ssoProcessor.ssoSession,
bytes.toReadPacket()
)
logger.verbose { "Decoded: ${raw.commandName}" }
raw
} else {
packetCodec.decodeRaw(
ssoProcessor.ssoSession,
packet
)
}
}
private suspend fun processBody(raw: RawIncomingPacket) {
packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" }
val result = packetCodec.processBody(context.bot, raw)
if (result == null) {
collectUnknownPacket(raw)
} else collectReceived(result)
}
fun send(packet: ByteReadPacket) {
queue.trySend(packet).onFailure {
throw it ?: throw IllegalStateException("Internal error: Failed to decode '$packet' without reason.")
}
}
}
@ -145,8 +200,6 @@ internal abstract class CommonNetworkHandler<Conn>(
this.setState { StateConnecting(ExceptionCollector()) }
?.resumeConnection()
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread.
println("INITIALIZED RETURN")
}
override fun toString(): String = "StateInitialized"
@ -214,9 +267,6 @@ internal abstract class CommonNetworkHandler<Conn>(
connectResult.await() // propagates exceptions
val connection = connection.await()
this.setState { StateLoading(connection) }
.also {
println(" this.setState { StateLoading(connection) }: " + it)
}
?.resumeConnection()
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread.
}

View File

@ -27,9 +27,12 @@ internal expect fun interface NetworkHandlerFactory<out H : NetworkHandler> {
}
}
internal expect abstract class SocketAddress {
val host: String
val port: Int
}
internal expect abstract class SocketAddress
internal expect fun SocketAddress(host: String, port: Int): SocketAddress
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
internal expect fun SocketAddress.getHost(): String
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
internal expect fun SocketAddress.getPort(): Int
internal expect fun createSocketAddress(host: String, port: Int): SocketAddress

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.utils.coroutineName
import net.mamoe.mirai.utils.debug
import net.mamoe.mirai.utils.systemProp
import kotlin.coroutines.coroutineContext
import kotlin.native.concurrent.ThreadLocal
internal class LoggingStateObserver(
val logger: MiraiLogger,
@ -72,6 +73,7 @@ internal class LoggingStateObserver(
)
}
@ThreadLocal
companion object {
/**
* - `on`/`true` for simple logging

View File

@ -422,5 +422,5 @@ internal fun String.toIpV4Long(): Long {
if (isEmpty()) return 0
val split = split('.')
if (split.size != 4) return 0
return split.mapToByteArray { it.toByte() }.toInt().toLongUnsigned()
return split.mapToByteArray { it.toUByte().toByte() }.toInt().toLongUnsigned()
}

View File

@ -9,7 +9,6 @@
package net.mamoe.mirai.internal.pipeline
import io.ktor.util.collections.*
import io.ktor.utils.io.core.*
import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext

View File

@ -0,0 +1,49 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal.utils
// internal for tests
internal object FileSystem {
fun checkLegitimacy(path: String) {
val char = path.firstOrNull { it in """:*?"<>|""" }
if (char != null) {
throw IllegalArgumentException("""Chars ':*?"<>|' are not allowed in path. RemoteFile path contains illegal char: '$char'. path='$path'""")
}
}
fun isLegal(path: String): Boolean {
return path.firstOrNull { it in """:*?"<>|""" } == null
}
fun normalize(path: String): String {
checkLegitimacy(path)
return path.replace('\\', '/')
}
// net.mamoe.mirai.internal.utils.internal.utils.FileSystemTest
fun normalize(parent: String, name: String): String {
var nName = normalize(name)
if (nName.startsWith('/')) return nName // absolute path then ignore parent
nName = nName.removeSuffix("/")
var nParent = normalize(parent)
if (nParent == "/") return "/$nName"
if (!nParent.startsWith('/')) nParent = "/$nParent"
val slash = nName.indexOf('/')
if (slash != -1) {
nParent += '/' + nName.substring(0, slash)
nName = nName.substring(slash + 1)
}
return "$nParent/$nName"
}
}

View File

@ -7,11 +7,17 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:JvmName("PlatformSocketKt_common")
package net.mamoe.mirai.internal.utils
import io.ktor.utils.io.core.*
import io.ktor.utils.io.errors.*
import net.mamoe.mirai.internal.network.handler.SocketAddress
import net.mamoe.mirai.internal.network.handler.getHost
import net.mamoe.mirai.internal.network.handler.getPort
import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel
import kotlin.jvm.JvmName
/**
* TCP Socket.
@ -32,7 +38,6 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel {
* @throws ReadPacketInternalException
*/
override suspend fun read(): ByteReadPacket
suspend fun connect(serverHost: String, serverPort: Int)
companion object {
suspend fun connect(
@ -48,6 +53,10 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel {
}
}
internal suspend inline fun PlatformSocket.Companion.connect(address: SocketAddress): PlatformSocket {
return connect(address.getHost(), address.getPort())
}
internal expect class SocketException : IOException {
constructor()

View File

@ -1,624 +0,0 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:Suppress("DEPRECATION", "OverridingDeprecatedMember")
package net.mamoe.mirai.internal.utils
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.groupCode
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind
import net.mamoe.mirai.internal.network.protocol
import net.mamoe.mirai.internal.network.protocol.data.proto.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH
import kotlin.contracts.contract
import kotlin.jvm.Volatile
private val fs = FileSystem
// internal for tests
internal object FileSystem {
fun checkLegitimacy(path: String) {
val char = path.firstOrNull { it in """:*?"<>|""" }
if (char != null) {
throw IllegalArgumentException("""Chars ':*?"<>|' are not allowed in path. RemoteFile path contains illegal char: '$char'. path='$path'""")
}
}
fun isLegal(path: String): Boolean {
return path.firstOrNull { it in """:*?"<>|""" } == null
}
fun normalize(path: String): String {
checkLegitimacy(path)
return path.replace('\\', '/')
}
// net.mamoe.mirai.internal.utils.internal.utils.FileSystemTest
fun normalize(parent: String, name: String): String {
var nName = normalize(name)
if (nName.startsWith('/')) return nName // absolute path then ignore parent
nName = nName.removeSuffix("/")
var nParent = normalize(parent)
if (nParent == "/") return "/$nName"
if (!nParent.startsWith('/')) nParent = "/$nParent"
val slash = nName.indexOf('/')
if (slash != -1) {
nParent += '/' + nName.substring(0, slash)
nName = nName.substring(slash + 1)
}
return "$nParent/$nName"
}
}
internal class RemoteFileInfo(
val id: String, // fileId or folderId
val isFile: Boolean,
val path: String,
val name: String,
val parentFolderId: String,
val size: Long,
val busId: Int, // for file only
val creatorId: Long, //ownerUin, createUin
val createTime: Long, // uploadTime, createTime
val modifyTime: Long,
val downloadTimes: Int,
val sha: ByteArray, // for file only
val md5: ByteArray, // for file only
) {
companion object {
val root = RemoteFileInfo(
"/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY
)
}
}
internal fun RemoteFile.checkIsImpl(): CommonRemoteFileImpl {
contract { returns() implies (this@checkIsImpl is RemoteFileImpl) }
return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.")
}
internal expect class RemoteFileImpl(
contact: Group,
path: String, // absolute
) : CommonRemoteFileImpl {
constructor(contact: Group, parent: String, name: String)
}
internal abstract class CommonRemoteFileImpl(
override val contact: Group,
override val path: String, // absolute
) : RemoteFile {
override var id: String? = null
override val name: String
get() = path.substringAfterLast('/')
private val bot get() = contact.bot.asQQAndroidBot()
private val client get() = bot.client
override val parent: CommonRemoteFileImpl?
get() {
if (path == ROOT_PATH) return null
val s = path.substringBeforeLast('/')
return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH })
}
/**
* Prefer id matching.
*/
private suspend fun Flow<Oidb0x6d8.GetFileListRspBody.Item>.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? {
var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null
val idMatching = firstOrNull {
if (it.name == this@CommonRemoteFileImpl.name) {
nameMatching = it
}
it.id == this@CommonRemoteFileImpl.id
}
return idMatching ?: nameMatching
}
private suspend fun getFileFolderInfo(): RemoteFileInfo? {
val parent = parent ?: return RemoteFileInfo.root
val info = parent.getFilesFlow()
.filter { it.name == this.name }
.findMatching()
?: return null
return when {
info.folderInfo != null -> info.folderInfo.run {
RemoteFileInfo(
id = folderId,
isFile = false,
path = path,
name = folderName,
parentFolderId = parentFolderId,
size = 0,
busId = 0,
creatorId = createUin,
createTime = createTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = 0,
sha = EMPTY_BYTE_ARRAY,
md5 = EMPTY_BYTE_ARRAY,
)
}
info.fileInfo != null -> info.fileInfo.run {
RemoteFileInfo(
id = fileId,
isFile = true,
path = path,
name = fileName,
parentFolderId = parentFolderId,
size = fileSize,
busId = busId,
creatorId = uploaderUin,
createTime = uploadTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = downloadTimes,
sha = sha,
md5 = md5,
)
}
else -> null
}
}
private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo {
if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.")
return this
}
override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile
// compiler bug
override suspend fun isDirectory(): Boolean = !isFile()
override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size
override suspend fun exists(): Boolean = this.getFileFolderInfo() != null
override suspend fun getInfo(): RemoteFile.FileInfo? {
return getFileFolderInfo()?.run {
RemoteFile.FileInfo(
name = name,
id = id,
path = path,
length = size,
downloadTimes = downloadTimes,
uploaderId = creatorId,
uploadTime = createTime,
lastModifyTime = modifyTime,
sha1 = sha,
md5 = md5,
)
}
}
private suspend fun getFilesFlow(): Flow<Oidb0x6d8.GetFileListRspBody.Item> {
val info = getFileFolderInfo() ?: return emptyFlow()
return flow {
var index = 0
while (true) {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = info.id,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
index += list.itemList.size
if (list.int32RetCode != 0) return@flow
if (list.itemList.isEmpty()) return@flow
emitAll(list.itemList.asFlow())
}
}
}
private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? {
val item = this
return when {
item.fileInfo != null -> {
resolve(item.fileInfo.fileName)
}
item.folderInfo != null -> {
resolve(item.folderInfo.folderName)
}
else -> null
}?.also {
it.id = item.id
}
}
override suspend fun listFiles(): Flow<RemoteFile> {
return getFilesFlow().mapNotNull { item ->
item.resolveToFile()
}
}
// compiler bug
override suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@OptIn(JavaFriendlyAPI::class)
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
if (!lazy) return listFiles().toList().iterator()
return object : Iterator<RemoteFile> {
private val queue = ArrayDeque<Oidb0x6d8.GetFileListRspBody.Item>(1)
@Volatile
private var index = 0
private var ended = false
private suspend fun updateItems() {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = path,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
if (list.int32RetCode != 0 || list.itemList.isEmpty()) {
ended = true
return
}
index += list.itemList.size
for (item in list.itemList) {
if (item.fileInfo != null || item.folderInfo != null) queue.add(item)
}
}
override fun hasNext(): Boolean {
if (queue.isEmpty() && !ended) runBlocking { updateItems() }
return queue.isNotEmpty()
}
override fun next(): RemoteFile {
return queue.removeFirst().resolveToFile()!!
}
}
}
override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative)
override fun resolve(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolve(relative.path).also { it.id = relative.id }
}
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
if (this.id == id) return this
val dirs = mutableListOf<Oidb0x6d8.GetFileListRspBody.Item>()
getFilesFlow().mapNotNull { item ->
when {
item.id == id -> item.resolveToFile()
deep && item.folderInfo != null -> {
dirs.add(item)
null
}
else -> null
}
}.firstOrNull()?.let { return it }
for (dir in dirs) {
dir.resolveToFile()?.resolveById(id, deep)?.let { return it }
}
return null
}
// compiler bug
override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
override fun resolveSibling(relative: String): RemoteFileImpl {
val parent = this.parent
if (parent == null) {
if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.")
return RemoteFileImpl(contact, ROOT_PATH)
}
return RemoteFileImpl(contact, parent.path, relative)
}
override fun resolveSibling(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolveSibling(relative.path).also { it.id = relative.id }
}
private fun RemoteFileInfo.isOperable(): Boolean =
creatorId == bot.id || contact.botPermission.isOperator()
private fun isBotOperator(): Boolean = contact.botPermission.isOperator()
override suspend fun delete(): Boolean {
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return when {
info.isFile -> {
bot.network.sendAndExpect(
FileManagement.DeleteFile(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id,
parentFolderId = info.parentFolderId,
)
).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0
}
// recursively -> {
// this.listFiles().collect { child ->
// child.delete()
// }
// this.delete()
// }
else -> {
// natively 'recursive'
bot.network.sendAndExpect(
FileManagement.DeleteFolder(
client, contact.id, info.id
)
).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0
}
}
}
override suspend fun renameTo(name: String): Boolean {
if (path == ROOT_PATH && name != ROOT_PATH) return false
val normalized = fs.normalize(name)
if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.")
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return bot.network.sendAndExpect(
if (info.isFile) {
FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized)
} else {
FileManagement.RenameFolder(client, contact.id, info.id, normalized)
}
).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0
}
/**
* null means not exist
*/
private suspend fun getIdSmart(): String? {
if (path == ROOT_PATH) return ROOT_PATH
return this.id ?: this.getFileFolderInfo()?.id
}
override suspend fun moveTo(target: RemoteFile): Boolean {
if (target.checkIsImpl().contact != this.contact) {
// TODO: 2021/3/4 cross-group file move
// target.mkdir()
// val targetFolderId = target.getIdSmart() ?: return false
// this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect {
// FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId)
// .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow()
//
// // TODO: 2021/3/3 batch packets
// }
// this.delete() // it is now empty
error("Cross-group file operation is not yet supported.")
}
if (target.path == this.path) return true
if (target.parent?.path == this.path) return false
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return if (info.isFile) {
val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false
bot.network.sendAndExpect(
FileManagement.MoveFile(
client,
contact.id,
info.busId,
info.id,
info.parentFolderId,
newParentId
)
).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
} else {
return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name))
.toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
}
}
override suspend fun mkdir(): Boolean {
if (path == ROOT_PATH) return false
if (!isBotOperator()) return false
val parentFolderId: String = parent?.getIdSmart() ?: return false
return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name))
.toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0
}
private suspend fun upload0(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose {
val parent = parent ?: return null
val parentInfo = parent.getFileFolderInfo() ?: return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestUpload(
client,
groupCode = contact.id,
folderId = parentInfo.id,
resource = resource,
filename = this.name
)
).toResult("RemoteFile.upload").getOrThrow()
if (resp.boolFileExist) {
return resp
}
val ext = GroupFileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = bot.id,
receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin?
groupCode = contact.groupCode,
),
fileEntry = ExcitingFileEntry(
fileSize = resource.size,
md5 = resource.md5,
sha1 = resource.sha1,
fileId = resp.fileId.toByteArray(),
uploadKey = resp.checkKey,
),
clientInfo = ExcitingClientInfo(
clientType = 2,
appId = client.protocol.id.toString(),
terminalType = 2,
clientVer = "9e9c09dc",
unknown = 4,
),
fileNameInfo = ExcitingFileNameInfo(this.name),
host = ExcitingHostConfig(
hosts = listOf(
ExcitingHostInfo(
url = ExcitingUrlInfo(
unknown = 1,
host = resp.uploadIpLanV4.firstOrNull()
?: resp.uploadIpLanV6.firstOrNull()
?: resp.uploadIp,
),
port = resp.uploadPort,
),
),
),
),
u3 = 0,
).toByteArray(GroupFileUploadExt.serializer())
callback?.onBegin(this, resource)
kotlin.runCatching {
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = ResourceKind.GROUP_FILE,
commandId = 71,
extendInfo = ext,
dataFlag = 0,
callback = if (callback == null) null else fun(it: Long) {
callback.onProgression(this, resource, it)
}
)
}.fold(
onSuccess = {
callback?.onSuccess(this, resource)
},
onFailure = {
callback?.onFailure(this, resource, it)
}
)
return resp
}
private suspend fun uploadInternal(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val resp = upload0(resource, callback) ?: error("Failed to upload file.")
return FileMessageImpl(
resp.fileId, resp.busId, name, resource.size, allowSend = true
)
}
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"),
level = DeprecationLevel.ERROR
)
override suspend fun upload(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val msg = uploadInternal(resource, callback)
contact.sendMessage(msg + MiraiInternalMessageFlag)
return msg
}
// compiler bug
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource)"),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
override suspend fun upload(resource: ExternalResource): FileMessage {
return upload(resource, null)
}
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
@Suppress("DEPRECATION")
return contact.sendMessage(uploadInternal(resource, null) + MiraiInternalMessageFlag)
}
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestDownload(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id
)
).toResult("RemoteFile.getDownloadInfo").getOrThrow()
return RemoteFile.DownloadInfo(
filename = name,
id = info.id,
path = path,
url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" +
info.id.toByteArray().toUHexString(""),
sha1 = info.sha,
md5 = info.md5
)
}
override fun toString(): String = path
override suspend fun toMessage(): FileMessage? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
return FileMessageImpl(info.id, info.busId, name, info.size)
}
}

View File

@ -15,9 +15,7 @@ import net.mamoe.mirai.utils.hexToBytes
internal expect interface ECDHPrivateKey
internal expect interface ECDHPublicKey {
fun getEncoded(): ByteArray
}
internal expect interface ECDHPublicKey
internal expect class ECDHKeyPairImpl : ECDHKeyPair
@ -48,9 +46,6 @@ internal interface ECDHKeyPair {
}
}
/**
* 椭圆曲线密码, ECDH 加密
*/
internal expect class ECDH(keyPair: ECDHKeyPair) {
val keyPair: ECDHKeyPair
@ -62,8 +57,11 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
companion object {
val isECDHAvailable: Boolean
/**
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
* This API is platform dependent.
* On JVM you need to add `signHead`,
* but on Native you need to provide a key with initial byte value 0x04 and of 65 bytes' length.
*/
fun constructPublicKey(key: ByteArray): ECDHPublicKey
@ -78,7 +76,7 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
/**
* 由一对密匙计算 shareKey
* 由一对密匙计算服务器需要的 shareKey
*/
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
}
@ -119,19 +117,12 @@ internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialP
@Serializable
internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
@Transient
internal val key: ECDHPublicKey = keyStr.adjustToPublicKey()
internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey()
}
internal expect val publicKeyForVerify: ECDHPublicKey
internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") }
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
internal fun String.adjustToPublicKey(): ECDHPublicKey {
return this.hexToBytes().adjustToPublicKey()
}
internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey
return ECDH.constructPublicKey(signHead + this)
}
internal val ECDH.Companion.curveName get() = "prime256v1" // p-256

View File

@ -12,14 +12,12 @@ package net.mamoe.mirai.internal.event
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.internal.test.runBlockingUnit
import kotlin.test.Test
import kotlin.test.assertIs
@JvmBlockingBridge
internal class EventChannelFlowTest : AbstractEventTest() {
@Test

View File

@ -21,7 +21,6 @@ import net.mamoe.mirai.event.events.MessageEvent
import kotlin.coroutines.coroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.test.*
internal class EventChannelTest : AbstractEventTest() {
@ -37,7 +36,7 @@ internal class EventChannelTest : AbstractEventTest() {
@Test
fun singleFilter() {
runBlocking {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.filterIsInstance<TE>()
.filter {
@ -69,7 +68,7 @@ internal class EventChannelTest : AbstractEventTest() {
@Test
fun multipleFilters() {
runBlocking {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.filterIsInstance<TE>()
.filter {
@ -109,7 +108,7 @@ internal class EventChannelTest : AbstractEventTest() {
fun multipleContexts1() {
runBlocking {
withContext(CoroutineName("1")) {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.context(CoroutineName("2"))
.context(CoroutineName("3"))
@ -132,7 +131,7 @@ internal class EventChannelTest : AbstractEventTest() {
fun multipleContexts2() {
runBlocking {
withContext(CoroutineName("1")) {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.context(CoroutineName("2"))
.context(CoroutineName("3"))
@ -156,7 +155,7 @@ internal class EventChannelTest : AbstractEventTest() {
fun multipleContexts3() {
runBlocking {
withContext(CoroutineName("1")) {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.context(CoroutineName("2"))
.subscribeOnce<TE> {
@ -178,7 +177,7 @@ internal class EventChannelTest : AbstractEventTest() {
fun multipleContexts4() {
runBlocking {
withContext(CoroutineName("1")) {
val received = suspendCoroutine<Int> { cont ->
val received = suspendCancellableCoroutine { cont ->
globalEventChannel()
.subscribeOnce<TE> {
assertEquals("1", currentCoroutineContext()[CoroutineName]!!.name)
@ -250,7 +249,8 @@ internal class EventChannelTest : AbstractEventTest() {
fun testExceptionInFilter() {
assertFailsWith<ExceptionInEventChannelFilterException> {
runBlocking {
suspendCoroutine<Int> { cont ->
@Suppress("RemoveExplicitTypeArguments")
suspendCancellableCoroutine<Int> { cont ->
globalEventChannel()
.exceptionHandler {
cont.resumeWithException(it)
@ -278,7 +278,7 @@ internal class EventChannelTest : AbstractEventTest() {
fun testExceptionInSubscribe() {
runBlocking {
assertFailsWith<IllegalStateException> {
suspendCoroutine<Int> { cont ->
suspendCancellableCoroutine<Int> { cont ->
val handler = CoroutineExceptionHandler { _, throwable ->
cont.resumeWithException(throwable)
}

View File

@ -60,10 +60,11 @@ internal class EventTests : AbstractEventTest() {
resetEventListeners()
var listeners = 0
val counter = atomic(0)
val channel = scope.globalEventChannel()
for (p in EventPriority.values()) {
repeat(2333) {
listeners++
scope.globalEventChannel().subscribeAlways<ParentEvent> {
channel.subscribeAlways<ParentEvent> {
counter.getAndIncrement()
}
}

View File

@ -21,21 +21,22 @@ internal class MessageProtocolFacadeTest : AbstractTest() {
assertEquals(
"""
QuoteReplyProtocol
AudioProtocol
CustomMessageProtocol
FaceProtocol
FileMessageProtocol
FlashImageProtocol
FaceProtocol
ImageProtocol
MarketFaceProtocol
MusicShareProtocol
PokeMessageProtocol
IgnoredMessagesProtocol
PttMessageProtocol
RichMessageProtocol
TextProtocol
VipFaceProtocol
ForwardMessageProtocol
LongMessageProtocol
IgnoredMessagesProtocol
UnsupportedMessageProtocol
GeneralMessageSenderProtocol
""".trimIndent(),

View File

@ -18,6 +18,7 @@ import net.mamoe.mirai.message.data.MessageSource.Key.quote
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.message.data.messageChainOf
import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.utils.hexToBytes
import kotlin.test.Test
@ -86,6 +87,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() {
),
),
),
srcMsg = EMPTY_BYTE_ARRAY
// mirai's OfflineMessageSource has no enough information to create 'srcMsg'
),
),
@ -319,6 +321,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() {
),
),
),
srcMsg = EMPTY_BYTE_ARRAY
// mirai's OfflineMessageSource has no enough information to create 'srcMsg'
),
),

View File

@ -14,6 +14,19 @@ import kotlin.test.Test
import kotlin.test.assertEquals
internal abstract class AbstractMutableComponentStorageTest : AbstractTest() {
internal data class TestComponent2(
val value: Int
) {
companion object : ComponentKey<TestComponent2>
}
internal data class TestComponent3(
val value: Int
) {
companion object : ComponentKey<TestComponent3>
}
protected abstract fun createStorage(): MutableComponentStorage
@Test

View File

@ -7,15 +7,30 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
@file:OptIn(TestOnly::class)
package net.mamoe.mirai.internal.network.component
import net.mamoe.mirai.internal.test.AbstractTest
import net.mamoe.mirai.utils.TestOnly
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertSame
internal class CombinedStorageTest : AbstractTest() {
internal data class TestComponent2(
val value: Int
) {
companion object : ComponentKey<TestComponent2>
}
internal data class TestComponent3(
val value: Int
) {
companion object : ComponentKey<TestComponent3>
}
@Test
fun `can get from main`() {
val storage = ConcurrentComponentStorage().apply {

View File

@ -24,11 +24,8 @@ import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
import net.mamoe.mirai.internal.network.component.setAll
import net.mamoe.mirai.internal.network.components.*
import net.mamoe.mirai.internal.network.framework.components.TestSsoProcessor
import net.mamoe.mirai.internal.network.handler.NetworkHandler
import net.mamoe.mirai.internal.network.handler.*
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory
import net.mamoe.mirai.internal.network.handler.SocketAddress
import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.utils.subLogger
@ -175,7 +172,7 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
//Use overrideComponents to avoid StackOverflowError when applying components
open fun createAddress(): SocketAddress =
overrideComponents[ServerList].pollAny().let { SocketAddress(it.host, it.port) }
overrideComponents[ServerList].pollAny().let { createSocketAddress(it.host, it.port) }
///////////////////////////////////////////////////////////////////////////
// Assertions

View File

@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.utils.TestOnly
import kotlin.test.Test
import kotlin.test.assertFails
import kotlin.test.assertTrue
/**
* Test whether the selector can recover the connection after first successful login.
@ -55,7 +56,7 @@ internal class SelectorRecoveryTest : AbstractCommonNHTestWithSelector() {
bot.components[EventDispatcher].joinBroadcast() // Wait our async connector to complete.
// BotOfflineMonitor immediately launches a recovery which is UNDISPATCHED, so connection is immediately recovered.
assertState(NetworkHandler.State.CONNECTING, NetworkHandler.State.LOADING, NetworkHandler.State.OK)
assertTrue { bot.network.state != NetworkHandler.State.CLOSED }
}
@Test

View File

@ -19,10 +19,7 @@ import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.events.GroupTempMessageEvent
import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC
import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.message.data.MessageSource
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.content
import net.mamoe.mirai.message.data.*
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
@ -124,7 +121,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time)
assertEquals(1230001, fromId)
assertEquals(2230203, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
}
assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content)
@ -205,7 +202,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time)
assertEquals(1230001, fromId)
assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
}
assertIs<PlainText>(get(1))
assertEquals("123", get(1).content)
@ -298,7 +295,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time)
assertEquals(1230001, fromId)
assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
}
assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content)
@ -392,7 +389,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time)
assertEquals(1230001, fromId)
assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
}
assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content)

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.internal.utils.crypto
import net.mamoe.mirai.internal.test.AbstractTest
import net.mamoe.mirai.utils.toUHexString
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
internal class ECDHTest : AbstractTest() {
@Test
fun `can generate key pair`() {
val alice = ECDH.generateKeyPair()
val bob = ECDH.generateKeyPair()
val aliceSecret = ECDH.calculateShareKey(alice.privateKey, bob.publicKey)
val bobSecret = ECDH.calculateShareKey(bob.privateKey, alice.publicKey)
println(aliceSecret.toUHexString())
assertContentEquals(aliceSecret, bobSecret)
}
@Test
fun `can get masked keys`() {
val alice = ECDH.generateKeyPair()
println(alice)
val maskedPublicKey = alice.maskedPublicKey
println(maskedPublicKey.toUHexString())
assertEquals(0x04, maskedPublicKey.first())
println(alice.maskedShareKey.toUHexString())
}
/*
EC_KEY *alice = create_key();
EC_KEY *bob = create_key();
assert(alice != NULL && bob != NULL);
const EC_POINT *alice_public = EC_KEY_get0_public_key(alice);
const EC_POINT *bob_public = EC_KEY_get0_public_key(bob);
size_t alice_secret_len;
size_t bob_secret_len;
unsigned char *alice_secret = get_secret(alice, bob_public, &alice_secret_len);
unsigned char *bob_secret = get_secret(bob, alice_public, &bob_secret_len);
assert(alice_secret != NULL && bob_secret != NULL
&& alice_secret_len == bob_secret_len);
for (int i = 0; i < alice_secret_len; i++)
assert(alice_secret[i] == bob_secret[i]);
EC_KEY_free(alice);
EC_KEY_free(bob);
OPENSSL_free(alice_secret);
OPENSSL_free(bob_secret);
return 0;
*/
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2019-2022 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.internal
import io.ktor.client.*
import io.ktor.client.engine.ios.*
import io.ktor.client.features.*
internal actual fun createDefaultHttpClient(): HttpClient {
return HttpClient(Ios) {
install(HttpTimeout) {
this.requestTimeoutMillis = 30_0000
this.connectTimeoutMillis = 30_0000
this.socketTimeoutMillis = 30_0000
}
}
}

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

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

View File

@ -0,0 +1,33 @@
/*
* 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:JvmName("MiraiImplKt")
package net.mamoe.mirai.internal
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.features.*
@Suppress("FunctionName")
internal actual fun _MiraiImpl_static_init() {
// nop
}
internal actual fun createDefaultHttpClient(): HttpClient {
return HttpClient(OkHttp) {
install(HttpTimeout) {
this.requestTimeoutMillis = 30_0000
this.connectTimeoutMillis = 30_0000
this.socketTimeoutMillis = 30_0000
}
}
}

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
*/
package net.mamoe.mirai.internal.contact
import net.mamoe.mirai.contact.ContactList
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.data.GroupInfo
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.utils.RemoteFileImpl
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.RemoteFile
import kotlin.coroutines.CoroutineContext
internal actual class GroupImpl actual constructor(
bot: QQAndroidBot,
parentCoroutineContext: CoroutineContext,
id: Long,
groupInfo: GroupInfo,
members: ContactList<NormalMemberImpl>,
) : Group, CommonGroupImpl(bot, parentCoroutineContext, id, groupInfo, members) {
actual companion object;
@Suppress("DEPRECATION")
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.WARNING)
@DeprecatedSinceMirai(warningSince = "2.8")
override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
}

View File

@ -12,8 +12,14 @@ package net.mamoe.mirai.internal.network.handler
import java.net.InetSocketAddress
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
internal actual typealias SocketAddress = java.net.SocketAddress
internal actual typealias SocketAddress = java.net.InetSocketAddress
internal actual fun SocketAddress(host: String, port: Int): SocketAddress {
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
internal actual fun SocketAddress.getHost(): String = hostString ?: error("Failed to get host from address '$this'.")
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
internal actual fun SocketAddress.getPort(): Int = this.port
internal actual fun createSocketAddress(host: String, port: Int): SocketAddress {
return InetSocketAddress.createUnresolved(host, port)
}

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.internal.network.impl.netty
import io.ktor.utils.io.core.*
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf
import io.netty.channel.*
@ -20,13 +21,11 @@ import io.netty.handler.codec.MessageToByteEncoder
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.job
import net.mamoe.mirai.internal.network.components.PacketCodec
import net.mamoe.mirai.internal.network.components.RawIncomingPacket
import net.mamoe.mirai.internal.network.components.SsoProcessor
import net.mamoe.mirai.internal.network.handler.CommonNetworkHandler
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.debug
import java.net.SocketAddress
import io.netty.channel.Channel as NettyChannel
@ -34,7 +33,7 @@ import io.netty.channel.Channel as NettyChannel
internal open class NettyNetworkHandler(
context: NetworkHandlerContext,
address: SocketAddress,
) : CommonNetworkHandler<NettyChannel>(context, address) {
) : CommonNetworkHandler<NettyChannel>(context, address.cast()) {
override fun toString(): String {
return "NettyNetworkHandler(context=$context, address=$address)"
}
@ -52,26 +51,11 @@ internal open class NettyNetworkHandler(
// netty conn.
///////////////////////////////////////////////////////////////////////////
private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] }
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
kotlin.runCatching {
ctx.fireChannelRead(msg.toReadPacket().use { packet ->
packetCodec.decodeRaw(ssoProcessor.ssoSession, packet)
})
}.onFailure { error ->
handleExceptionInDecoding(error)
}
}
}
private inner class RawIncomingPacketCollector(
private inner class IncomingPacketDecoder(
private val decodePipeline: PacketDecodePipeline,
) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) {
override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) {
decodePipeline.send(msg)
) : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
decodePipeline.send(msg.toReadPacket())
}
}
@ -91,8 +75,7 @@ internal open class NettyNetworkHandler(
})
.addLast("outgoing-packet-encoder", OutgoingPacketEncoder())
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4))
.addLast(ByteBufToIncomingPacketDecoder())
.addLast("raw-packet-collector", RawIncomingPacketCollector(decodePipeline))
.addLast(IncomingPacketDecoder(decodePipeline))
}
protected open fun createDummyDecodePipeline() = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)

View File

@ -11,7 +11,7 @@ package net.mamoe.mirai.internal.network.impl.netty
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory
import java.net.SocketAddress
import net.mamoe.mirai.internal.network.handler.SocketAddress
internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> {
override fun create(context: NetworkHandlerContext, address: SocketAddress): NettyNetworkHandler {

View File

@ -86,7 +86,7 @@ internal actual class PlatformSocket : Closeable, HighwayProtocolChannel {
}
}
actual suspend fun connect(serverHost: String, serverPort: Int) {
suspend fun connect(serverHost: String, serverPort: Int) {
runInterruptible(Dispatchers.IO) {
socket = Socket(serverHost, serverPort)
readChannel = socket.getInputStream().buffered()

View File

@ -11,13 +11,580 @@
package net.mamoe.mirai.internal.utils
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.contact.Contact
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.contact.isOperator
import net.mamoe.mirai.internal.asQQAndroidBot
import net.mamoe.mirai.internal.contact.groupCode
import net.mamoe.mirai.internal.message.data.FileMessageImpl
import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag
import net.mamoe.mirai.internal.network.highway.Highway
import net.mamoe.mirai.internal.network.highway.ResourceKind
import net.mamoe.mirai.internal.network.protocol
import net.mamoe.mirai.internal.network.protocol.data.proto.*
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage
import net.mamoe.mirai.utils.*
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.RemoteFile
import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH
import java.io.File
import kotlin.contracts.contract
import kotlin.text.toByteArray
private val fs = FileSystem
internal class RemoteFileInfo(
val id: String, // fileId or folderId
val isFile: Boolean,
val path: String,
val name: String,
val parentFolderId: String,
val size: Long,
val busId: Int, // for file only
val creatorId: Long, //ownerUin, createUin
val createTime: Long, // uploadTime, createTime
val modifyTime: Long,
val downloadTimes: Int,
val sha: ByteArray, // for file only
val md5: ByteArray, // for file only
) {
companion object {
val root = RemoteFileInfo(
"/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY
)
}
}
internal fun RemoteFile.checkIsImpl(): CommonRemoteFileImpl {
contract { returns() implies (this@checkIsImpl is RemoteFileImpl) }
return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.")
}
internal expect class RemoteFileImpl(
contact: Group,
path: String, // absolute
) : CommonRemoteFileImpl {
constructor(contact: Group, parent: String, name: String)
}
internal abstract class CommonRemoteFileImpl(
override val contact: Group,
override val path: String, // absolute
) : RemoteFile {
override var id: String? = null
override val name: String
get() = path.substringAfterLast('/')
private val bot get() = contact.bot.asQQAndroidBot()
private val client get() = bot.client
override val parent: CommonRemoteFileImpl?
get() {
if (path == ROOT_PATH) return null
val s = path.substringBeforeLast('/')
return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH })
}
/**
* Prefer id matching.
*/
private suspend fun Flow<Oidb0x6d8.GetFileListRspBody.Item>.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? {
var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null
val idMatching = firstOrNull {
if (it.name == this@CommonRemoteFileImpl.name) {
nameMatching = it
}
it.id == this@CommonRemoteFileImpl.id
}
return idMatching ?: nameMatching
}
private suspend fun getFileFolderInfo(): RemoteFileInfo? {
val parent = parent ?: return RemoteFileInfo.root
val info = parent.getFilesFlow()
.filter { it.name == this.name }
.findMatching()
?: return null
return when {
info.folderInfo != null -> info.folderInfo.run {
RemoteFileInfo(
id = folderId,
isFile = false,
path = path,
name = folderName,
parentFolderId = parentFolderId,
size = 0,
busId = 0,
creatorId = createUin,
createTime = createTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = 0,
sha = EMPTY_BYTE_ARRAY,
md5 = EMPTY_BYTE_ARRAY,
)
}
info.fileInfo != null -> info.fileInfo.run {
RemoteFileInfo(
id = fileId,
isFile = true,
path = path,
name = fileName,
parentFolderId = parentFolderId,
size = fileSize,
busId = busId,
creatorId = uploaderUin,
createTime = uploadTime.toLongUnsigned(),
modifyTime = modifyTime.toLongUnsigned(),
downloadTimes = downloadTimes,
sha = sha,
md5 = md5,
)
}
else -> null
}
}
private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo {
if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.")
return this
}
override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile
// compiler bug
override suspend fun isDirectory(): Boolean = !isFile()
override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size
override suspend fun exists(): Boolean = this.getFileFolderInfo() != null
override suspend fun getInfo(): RemoteFile.FileInfo? {
return getFileFolderInfo()?.run {
RemoteFile.FileInfo(
name = name,
id = id,
path = path,
length = size,
downloadTimes = downloadTimes,
uploaderId = creatorId,
uploadTime = createTime,
lastModifyTime = modifyTime,
sha1 = sha,
md5 = md5,
)
}
}
private suspend fun getFilesFlow(): Flow<Oidb0x6d8.GetFileListRspBody.Item> {
val info = getFileFolderInfo() ?: return emptyFlow()
return flow {
var index = 0
while (true) {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = info.id,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
index += list.itemList.size
if (list.int32RetCode != 0) return@flow
if (list.itemList.isEmpty()) return@flow
emitAll(list.itemList.asFlow())
}
}
}
private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? {
val item = this
return when {
item.fileInfo != null -> {
resolve(item.fileInfo.fileName)
}
item.folderInfo != null -> {
resolve(item.folderInfo.folderName)
}
else -> null
}?.also {
it.id = item.id
}
}
override suspend fun listFiles(): Flow<RemoteFile> {
return getFilesFlow().mapNotNull { item ->
item.resolveToFile()
}
}
// compiler bug
override suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@OptIn(JavaFriendlyAPI::class)
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
if (!lazy) return listFiles().toList().iterator()
return object : Iterator<RemoteFile> {
private val queue = ArrayDeque<Oidb0x6d8.GetFileListRspBody.Item>(1)
@Volatile
private var index = 0
private var ended = false
private suspend fun updateItems() {
val list = bot.network.sendAndExpect(
FileManagement.GetFileList(
client,
groupCode = contact.id,
folderId = path,
startIndex = index
)
).toResult("RemoteFile.listFiles").getOrThrow()
if (list.int32RetCode != 0 || list.itemList.isEmpty()) {
ended = true
return
}
index += list.itemList.size
for (item in list.itemList) {
if (item.fileInfo != null || item.folderInfo != null) queue.add(item)
}
}
override fun hasNext(): Boolean {
if (queue.isEmpty() && !ended) runBlocking { updateItems() }
return queue.isNotEmpty()
}
override fun next(): RemoteFile {
return queue.removeFirst().resolveToFile()!!
}
}
}
override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative)
override fun resolve(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolve(relative.path).also { it.id = relative.id }
}
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
if (this.id == id) return this
val dirs = mutableListOf<Oidb0x6d8.GetFileListRspBody.Item>()
getFilesFlow().mapNotNull { item ->
when {
item.id == id -> item.resolveToFile()
deep && item.folderInfo != null -> {
dirs.add(item)
null
}
else -> null
}
}.firstOrNull()?.let { return it }
for (dir in dirs) {
dir.resolveToFile()?.resolveById(id, deep)?.let { return it }
}
return null
}
// compiler bug
override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
override fun resolveSibling(relative: String): RemoteFileImpl {
val parent = this.parent
if (parent == null) {
if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.")
return RemoteFileImpl(contact, ROOT_PATH)
}
return RemoteFileImpl(contact, parent.path, relative)
}
override fun resolveSibling(relative: RemoteFile): RemoteFileImpl {
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
return resolveSibling(relative.path).also { it.id = relative.id }
}
private fun RemoteFileInfo.isOperable(): Boolean =
creatorId == bot.id || contact.botPermission.isOperator()
private fun isBotOperator(): Boolean = contact.botPermission.isOperator()
override suspend fun delete(): Boolean {
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return when {
info.isFile -> {
bot.network.sendAndExpect(
FileManagement.DeleteFile(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id,
parentFolderId = info.parentFolderId,
)
).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0
}
// recursively -> {
// this.listFiles().collect { child ->
// child.delete()
// }
// this.delete()
// }
else -> {
// natively 'recursive'
bot.network.sendAndExpect(
FileManagement.DeleteFolder(
client, contact.id, info.id
)
).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0
}
}
}
override suspend fun renameTo(name: String): Boolean {
if (path == ROOT_PATH && name != ROOT_PATH) return false
val normalized = fs.normalize(name)
if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.")
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return bot.network.sendAndExpect(
if (info.isFile) {
FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized)
} else {
FileManagement.RenameFolder(client, contact.id, info.id, normalized)
}
).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0
}
/**
* null means not exist
*/
private suspend fun getIdSmart(): String? {
if (path == ROOT_PATH) return ROOT_PATH
return this.id ?: this.getFileFolderInfo()?.id
}
override suspend fun moveTo(target: RemoteFile): Boolean {
if (target.checkIsImpl().contact != this.contact) {
// TODO: 2021/3/4 cross-group file move
// target.mkdir()
// val targetFolderId = target.getIdSmart() ?: return false
// this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect {
// FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId)
// .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow()
//
// // TODO: 2021/3/3 batch packets
// }
// this.delete() // it is now empty
error("Cross-group file operation is not yet supported.")
}
if (target.path == this.path) return true
if (target.parent?.path == this.path) return false
val info = getFileFolderInfo() ?: return false
if (!info.isOperable()) return false
return if (info.isFile) {
val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false
bot.network.sendAndExpect(
FileManagement.MoveFile(
client,
contact.id,
info.busId,
info.id,
info.parentFolderId,
newParentId
)
).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
} else {
return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name))
.toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
}
}
override suspend fun mkdir(): Boolean {
if (path == ROOT_PATH) return false
if (!isBotOperator()) return false
val parentFolderId: String = parent?.getIdSmart() ?: return false
return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name))
.toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0
}
private suspend fun upload0(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose {
val parent = parent ?: return null
val parentInfo = parent.getFileFolderInfo() ?: return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestUpload(
client,
groupCode = contact.id,
folderId = parentInfo.id,
resource = resource,
filename = this.name
)
).toResult("RemoteFile.upload").getOrThrow()
if (resp.boolFileExist) {
return resp
}
val ext = GroupFileUploadExt(
u1 = 100,
u2 = 1,
entry = GroupFileUploadEntry(
business = ExcitingBusiInfo(
busId = resp.busId,
senderUin = bot.id,
receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin?
groupCode = contact.groupCode,
),
fileEntry = ExcitingFileEntry(
fileSize = resource.size,
md5 = resource.md5,
sha1 = resource.sha1,
fileId = resp.fileId.toByteArray(),
uploadKey = resp.checkKey,
),
clientInfo = ExcitingClientInfo(
clientType = 2,
appId = client.protocol.id.toString(),
terminalType = 2,
clientVer = "9e9c09dc",
unknown = 4,
),
fileNameInfo = ExcitingFileNameInfo(this.name),
host = ExcitingHostConfig(
hosts = listOf(
ExcitingHostInfo(
url = ExcitingUrlInfo(
unknown = 1,
host = resp.uploadIpLanV4.firstOrNull()
?: resp.uploadIpLanV6.firstOrNull()
?: resp.uploadIp,
),
port = resp.uploadPort,
),
),
),
),
u3 = 0,
).toByteArray(GroupFileUploadExt.serializer())
callback?.onBegin(this, resource)
kotlin.runCatching {
Highway.uploadResourceBdh(
bot = bot,
resource = resource,
kind = ResourceKind.GROUP_FILE,
commandId = 71,
extendInfo = ext,
dataFlag = 0,
callback = if (callback == null) null else fun(it: Long) {
callback.onProgression(this, resource, it)
}
)
}.fold(
onSuccess = {
callback?.onSuccess(this, resource)
},
onFailure = {
callback?.onFailure(this, resource, it)
}
)
return resp
}
private suspend fun uploadInternal(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val resp = upload0(resource, callback) ?: error("Failed to upload file.")
return FileMessageImpl(
resp.fileId, resp.busId, name, resource.size, allowSend = true
)
}
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"),
level = DeprecationLevel.ERROR
)
override suspend fun upload(
resource: ExternalResource,
callback: RemoteFile.ProgressionCallback?,
): FileMessage {
val msg = uploadInternal(resource, callback)
contact.sendMessage(msg + MiraiInternalMessageFlag)
return msg
}
// compiler bug
@Deprecated(
"Use uploadAndSend instead.",
replaceWith = ReplaceWith("this.uploadAndSend(resource)"),
level = DeprecationLevel.ERROR
)
@Suppress("DEPRECATION_ERROR")
override suspend fun upload(resource: ExternalResource): FileMessage {
return upload(resource, null)
}
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
@Suppress("DEPRECATION")
return contact.sendMessage(uploadInternal(resource, null) + MiraiInternalMessageFlag)
}
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
val resp = bot.network.sendAndExpect(
FileManagement.RequestDownload(
client,
groupCode = contact.id,
busId = info.busId,
fileId = info.id
)
).toResult("RemoteFile.getDownloadInfo").getOrThrow()
return RemoteFile.DownloadInfo(
filename = name,
id = info.id,
path = path,
url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" +
info.id.toByteArray().toUHexString(""),
sha1 = info.sha,
md5 = info.md5
)
}
override fun toString(): String = path
override suspend fun toMessage(): FileMessage? {
val info = getFileFolderInfo() ?: return null
if (!info.isFile) return null
return FileMessageImpl(info.id, info.busId, name, info.size)
}
}
internal actual class RemoteFileImpl actual constructor(
contact: Group,

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.internal.utils.crypto
import net.mamoe.mirai.utils.decodeBase64
import net.mamoe.mirai.utils.hexToBytes
import java.security.KeyFactory
import java.security.KeyPair
import java.security.PrivateKey
@ -36,8 +37,13 @@ internal actual class ECDHKeyPairImpl(
}
internal actual val publicKeyForVerify: ECDHPublicKey by lazy {
internal val publicKeyForVerify: ECDHPublicKey by lazy {
KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64()))
}
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
return ECDH.constructPublicKey(signHead + this)
}

View File

@ -12,19 +12,20 @@ package net.mamoe.mirai.internal.network.component
import kotlin.test.Test
import kotlin.test.assertEquals
internal data class TestComponent2(
val value: Int
) {
companion object : ComponentKey<TestComponent2>
}
internal data class TestComponent3(
val value: Int
) {
companion object : ComponentKey<TestComponent3>
}
internal class ConcurrentComponentStorageTest : AbstractMutableComponentStorageTest() {
internal data class TestComponent2(
val value: Int
) {
companion object : ComponentKey<TestComponent2>
}
internal data class TestComponent3(
val value: Int
) {
companion object : ComponentKey<TestComponent3>
}
override fun createStorage(): MutableComponentStorage = ConcurrentComponentStorage(showAllComponents = true)
@Test

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