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.api.attributes.Attribute
import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getting import org.gradle.kotlin.dsl.getting
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 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.MAIN_COMPILATION_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_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 UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS }
val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
fun Project.configureHMPPJvm() { fun Project.configureHMPPJvm() {
extensions.getByType(KotlinMultiplatformExtension::class.java).apply { 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) jvmBaseMain.dependsOn(commonMain)
jvmBaseTest.dependsOn(commonTest) jvmBaseTest.dependsOn(commonTest)

View File

@ -31,7 +31,7 @@ object Versions {
const val coroutines = "1.6.2" const val coroutines = "1.6.2"
const val atomicFU = "0.17.2" const val atomicFU = "0.17.2"
const val serialization = "1.3.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" const val binaryValidator = "0.4.0"

View File

@ -24,3 +24,5 @@ mirai.android.target.api.level=24
# Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions. # Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions.
systemProp.use.maven.local=false 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 test: a
""".trimIndent() """.trimIndent()
) )
File("data/PluginDataRenameToIdTest").mkdirs()
File("data/PluginDataRenameToIdTest/test.txt").createNewFile() File("data/PluginDataRenameToIdTest/test.txt").createNewFile()
File("data/PluginDataRenameToIdTest/testdata.yml").writeText( 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 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 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;)Ljava/lang/Void;
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V 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 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 { 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 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 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 contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun containsAll (Ljava/util/Collection;)Z public fun containsAll (Ljava/util/Collection;)Z
public fun contentToString ()Ljava/lang/String; public fun contentToString ()Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object; public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage; public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z public fun getHasConstrainSingle ()Z
public fun getSize ()I 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 indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun isEmpty ()Z public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator; 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 lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun listIterator ()Ljava/util/ListIterator; public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)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 fun serializeToMiraiCode ()Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer; 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 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; 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 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 { 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 public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun error0 (Ljava/lang/String;)V protected fun error0 (Ljava/lang/String;)V
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)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;)V
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun info0 (Ljava/lang/String;)V protected fun info0 (Ljava/lang/String;)V
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun isEnabled ()Z 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;)V
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun verbose0 (Ljava/lang/String;)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;)V
public fun error (Ljava/lang/String;Ljava/lang/Throwable;)V public fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun error (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 getIdentity ()Ljava/lang/String;
public fun info (Ljava/lang/String;)V public fun info (Ljava/lang/String;)V
public fun info (Ljava/lang/String;Ljava/lang/Throwable;)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 isVerboseEnabled ()Z
public fun isWarningEnabled ()Z public fun isWarningEnabled ()Z
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger; 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;)V
public fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V public fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun verbose (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 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 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;)Ljava/lang/Void;
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V 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;)Ljava/lang/Void;
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V 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 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 { 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 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 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 contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
public fun containsAll (Ljava/util/Collection;)Z public fun containsAll (Ljava/util/Collection;)Z
public fun contentToString ()Ljava/lang/String; public fun contentToString ()Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public synthetic fun get (I)Ljava/lang/Object; public synthetic fun get (I)Ljava/lang/Object;
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage; public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
public fun getHasConstrainSingle ()Z public fun getHasConstrainSingle ()Z
public fun getSize ()I 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 indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun isEmpty ()Z public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator; 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 lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
public fun listIterator ()Ljava/util/ListIterator; public fun listIterator ()Ljava/util/ListIterator;
public fun listIterator (I)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 fun serializeToMiraiCode ()Ljava/lang/String;
public final fun serializer ()Lkotlinx/serialization/KSerializer; 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 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; 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 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 { 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 public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun error0 (Ljava/lang/String;)V protected fun error0 (Ljava/lang/String;)V
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)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;)V
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun info0 (Ljava/lang/String;)V protected fun info0 (Ljava/lang/String;)V
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun isEnabled ()Z 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;)V
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
protected fun verbose0 (Ljava/lang/String;)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 * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
package net.mamoe.mirai.internal.utils package net.mameo.mirai
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))
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 Mamoe Technologies and contributors. * Copyright 2019-2022 Mamoe Technologies and contributors.
* *
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -11,7 +11,6 @@
package net.mamoe.mirai.contact package net.mamoe.mirai.contact
import net.mamoe.mirai.contact.file.RemoteFiles import net.mamoe.mirai.contact.file.RemoteFiles
import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.NotStableForInheritance
/** /**
@ -24,17 +23,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
* @see RemoteFiles * @see RemoteFiles
*/ */
@NotStableForInheritance @NotStableForInheritance
public interface FileSupported : Contact { public expect 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
/** /**
* 获取远程文件列表 (管理器). * 获取远程文件列表 (管理器).
* *

View File

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

View File

@ -7,12 +7,9 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmBlockingBridge
package net.mamoe.mirai.contact.roaming package net.mamoe.mirai.contact.roaming
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Friend import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSource 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> 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] 的父作用域. * [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 * @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.event.VerboseEvent
import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.utils.MiraiInternalApi import net.mamoe.mirai.utils.MiraiInternalApi
import kotlin.jvm.JvmField
import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
@ -83,7 +82,6 @@ public data class NewFriendRequestEvent @MiraiInternalApi public constructor(
*/ */
public val fromNick: String, public val fromNick: String,
) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent { ) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent {
@JvmField
internal val responded: AtomicBoolean = atomic(false) 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.DeprecatedSinceMirai
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi 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] 就已删除这个群. * 机器人被踢出群或在其他客户端主动退出一个群. 在事件广播前 [Bot.groups] 就已删除这个群.
@ -354,7 +357,6 @@ public data class BotInvitedJoinGroupRequestEvent @MiraiInternalApi constructor(
*/ */
public val invitor: Friend? get() = this.bot.getFriend(invitorId) public val invitor: Friend? get() = this.bot.getFriend(invitorId)
@JvmField
internal val responded: AtomicBoolean = atomic(false) internal val responded: AtomicBoolean = atomic(false)
@JvmBlockingBridge @JvmBlockingBridge
@ -403,8 +405,6 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor(
*/ */
public val invitor: NormalMember? by lazy { invitorId?.let { group?.get(it) } } public val invitor: NormalMember? by lazy { invitorId?.let { group?.get(it) } }
@JvmField
@PublishedApi
internal val responded: AtomicBoolean = atomic(false) internal val responded: AtomicBoolean = atomic(false)
/** /**

View File

@ -14,8 +14,6 @@ package net.mamoe.mirai.event
import kotlinx.coroutines.* import kotlinx.coroutines.*
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.Message 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.isContextIdenticalWith
import net.mamoe.mirai.message.nextMessage import net.mamoe.mirai.message.nextMessage
import net.mamoe.mirai.utils.MiraiExperimentalApi 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") 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 @JvmInline
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") @Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
public value class MessageSelectionTimeoutChecker internal constructor(public val timeoutMillis: Long) 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.contact.file.AbsoluteFile
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.code.CodableMessage 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.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.JvmMultifileClass
import kotlin.jvm.JvmName import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic import kotlin.jvm.JvmStatic
@ -46,10 +48,11 @@ import kotlin.jvm.JvmSynthetic
* @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定. * @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定.
*/ */
@Serializable(FileMessage.Serializer::class) @Serializable(FileMessage.Serializer::class)
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
@SerialName(FileMessage.SERIAL_NAME) @SerialName(FileMessage.SERIAL_NAME)
@NotStableForInheritance @NotStableForInheritance
@JvmBlockingBridge @JvmBlockingBridge
public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
/** /**
* 服务器需要的某种 ID. * 服务器需要的某种 ID.
*/ */
@ -70,76 +73,58 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
*/ */
public val size: Long public val size: Long
override fun contentToString(): String = "[文件]$name" // orthodox open override fun contentToString(): String
override fun appendMiraiCodeTo(builder: StringBuilder) { open 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`. * 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `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`.
* *
* @since 2.8 * @since 2.8
*/ */
public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
override val key: Key get() = Key open override val key: Key
@MiraiInternalApi @MiraiInternalApi
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R { open override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R
return visitor.visitFileMessage(this, data)
}
/** /**
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更. * 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
*/ */
public companion object Key : public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, FileMessage>( AbstractPolymorphicMessageKey<MessageContent, FileMessage> {
MessageContent, { it.safeCast() }) {
public const val SERIAL_NAME: String = "FileMessage" @Suppress("CONST_VAL_WITHOUT_INITIALIZER")
public const val SERIAL_NAME: String
/** /**
* 构造 [FileMessage] * 构造 [FileMessage]
* @since 2.5 * @since 2.5
*/ */
@JvmStatic @JvmStatic
public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage = public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage
Mirai.createFileMessage(id, internalId, name, size)
} }
public object Serializer : KSerializer<FileMessage> by FallbackSerializer(SERIAL_NAME) // not polymorphic public object Serializer : KSerializer<FileMessage> // not polymorphic
}
@MiraiInternalApi @MiraiInternalApi
private open class FallbackSerializer(serialName: String) : KSerializer<FileMessage> by Delegate.serializer().map( internal open class FallbackFileMessageSerializer constructor(serialName: String) :
KSerializer<FileMessage> by Delegate.serializer().map(
Delegate.serializer().descriptor.copy(serialName), Delegate.serializer().descriptor.copy(serialName),
serialize = { Delegate(id, internalId, name, size) }, serialize = { Delegate(id, internalId, name, size) },
deserialize = { Mirai.createFileMessage(id, internalId, name, size) }, deserialize = { Mirai.createFileMessage(id, internalId, name, size) },
) { ) {
@SerialName(SERIAL_NAME) @Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
@Serializable @SerialName(FileMessage.SERIAL_NAME)
data class Delegate( @Serializable
val id: String, data class Delegate constructor(
val internalId: Int, val id: String,
val name: String, val internalId: Int,
val size: Long, 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.GlobalEventChannel
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.event.syncFromEvent import net.mamoe.mirai.event.syncFromEvent
import net.mamoe.mirai.event.syncFromEventOrNull
import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageChain
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
@ -74,7 +73,7 @@ public suspend inline fun <reified P : MessageEvent> P.nextMessage(
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值 * @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值
* @return 消息链. 超时时返回 `null` * @return 消息链. 超时时返回 `null`
* *
* @see syncFromEventOrNull 实现原理 * @see syncFromEvent
*/ */
@JvmSynthetic @JvmSynthetic
public suspend inline fun <reified P : MessageEvent> P.nextMessageOrNull( 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` 时还会深入子目录查找. * 精确获取 [AbsoluteFile.id] [id] 的文件. 在目标文件不存在时返回 `null`. [deep] `true` 时还会深入子目录查找.
*/ */
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
@JvmOverloads
public actual suspend fun resolveFileById( public actual suspend fun resolveFileById(
id: String, id: String,
deep: Boolean deep: Boolean = false
): AbsoluteFile? ): AbsoluteFile?
/** /**
@ -187,10 +189,12 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder {
* *
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
*/ */
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
@JvmOverloads
public actual suspend fun uploadNewFile( public actual suspend fun uploadNewFile(
filepath: String, filepath: String,
content: ExternalResource, content: ExternalResource,
callback: ProgressionCallback<AbsoluteFile, Long>?, callback: ProgressionCallback<AbsoluteFile, Long>? = null,
): AbsoluteFile ): AbsoluteFile
public actual companion object { public actual companion object {

View File

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

View File

@ -17,8 +17,6 @@ import net.mamoe.mirai.utils.DeprecatedSinceMirai
import net.mamoe.mirai.utils.MiraiExperimentalApi import net.mamoe.mirai.utils.MiraiExperimentalApi
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext 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 net.mamoe.mirai.utils.DeprecatedSinceMirai
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.jvm.JvmName
import kotlin.jvm.JvmSynthetic
import kotlin.reflect.KClass 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(".") 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 // Coroutines
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -341,7 +321,7 @@ public actual open class BotConfiguration { // open for Java
@ConfigurationDsl @ConfigurationDsl
public actual fun loadDeviceInfoJson(json: String) { public actual fun loadDeviceInfoJson(json: String) {
deviceInfo = { 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 // To structural order
new.workingDir = workingDir new.workingDir = workingDir
@Suppress("DEPRECATION_ERROR") @Suppress("DEPRECATION_ERROR")
new.json = json
new.parentCoroutineContext = parentCoroutineContext new.parentCoroutineContext = parentCoroutineContext
new.heartbeatPeriodMillis = heartbeatPeriodMillis new.heartbeatPeriodMillis = heartbeatPeriodMillis
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
@ -646,6 +625,20 @@ public actual open class BotConfiguration { // open for Java
@JvmStatic @JvmStatic
public actual val Default: BotConfiguration = BotConfiguration() 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 { internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
return { return {
@Suppress("DEPRECATION_ERROR") @Suppress("DEPRECATION_ERROR")

View File

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

View File

@ -57,11 +57,11 @@ public actual interface MiraiLogger {
public actual companion object INSTANCE : Factory by loadService(Factory::class, fallbackImplementation = { public actual companion object INSTANCE : Factory by loadService(Factory::class, fallbackImplementation = {
object : Factory { object : Factory {
override fun create(requester: KClass<*>): MiraiLogger { 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 { 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 { dependencies {
} }
} }

View File

@ -14,11 +14,11 @@ import io.ktor.utils.io.pool.*
/** /**
* 缓存 [ByteArray] 实例的 [ObjectPool] * 缓存 [ByteArray] 实例的 [ObjectPool]
*/ */
public object ByteArrayPool : DefaultPool<ByteArray>(256) { public object ByteArrayPool : DefaultPool<ByteArray>(128) {
/** /**
* 每一个 [ByteArray] 的大小 * 每一个 [ByteArray] 的大小
*/ */
public const val BUFFER_SIZE: Int = 8192 * 8 public const val BUFFER_SIZE: Int = 4096
override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE) 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 = public inline fun ByteArray.toReadPacket(
ByteReadPacket(this, offset = offset, length = length) 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 { public inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
contract { 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()) { public fun Short.toByteArray(): ByteArray = with(toInt()) {
byteArrayOf( 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( public fun Int.toByteArray(): ByteArray = byteArrayOf(
ushr(24).toByte(), 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( public fun Long.toByteArray(): ByteArray = byteArrayOf(
(ushr(56) and 0xFF).toByte(), (ushr(56) and 0xFF).toByte(),
@ -56,10 +56,13 @@ public fun Long.toByteArray(): ByteArray = byteArrayOf(
(ushr(0) and 0xFF).toByte() (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) 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()) { public fun UShort.toByteArray(): ByteArray = with(toUInt()) {
byteArrayOf( 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) 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 = public fun UShort.toUHexString(separator: String = " "): String =
this.toInt().shr(8).toUShort().toUByte().toUHexString() + separator + this.toUByte().toUHexString() 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 = public fun ULong.toUHexString(separator: String = " "): String =
this.toLong().toUHexString(separator) this.toLong().toUHexString(separator)
/**
* Converts a Long to its hex representation in network order (big-endian).
*/
public fun Long.toUHexString(separator: String = " "): String = public fun Long.toUHexString(separator: String = " "): String =
this.ushr(32).toUInt().toUHexString(separator) + separator + this.toUInt().toUHexString(separator) 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() 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) 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() if (this.toInt() in 0..15) "0${this.toString(16).uppercase()}" else this.toString(16).uppercase()
/** /**
* [this] 4 [Byte] bits 合并为一个 [Int] * Converts 4 bytes to an UInt in network order (big-endian).
*
* 详细解释:
* 一个 [Byte] 8 bits
* 一个 [Int] 32 bits
* 本函数将 4 [Byte] bits 连接得到 [Int]
*/ */
public fun ByteArray.toUInt(): UInt = public fun ByteArray.toUInt(): UInt =
(this[0].toUInt().and(255u) shl 24) + (this[1].toUInt().and(255u) shl 16) + (this[2].toUInt() (this[0].toUInt().and(255u) shl 24)
.and(255u) shl 8) + (this[3].toUInt().and( .plus(this[1].toUInt().and(255u) shl 16)
255u .plus(this[2].toUInt().and(255u) shl 8)
) shl 0) .plus(this[3].toUInt().and(255u) shl 0)
/**
* Converts 2 bytes to an UShort in network order (big-endian).
*/
public fun ByteArray.toUShort(): UShort = public fun ByteArray.toUShort(): UShort =
((this[0].toUInt().and(255u) shl 8) + (this[1].toUInt().and(255u) shl 0)).toUShort() ((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 = public fun ByteArray.toInt(offset: Int = 0): Int =
this[offset + 0].toInt().and(255).shl(24) this[offset + 0].toInt().and(255).shl(24)
.plus(this[offset + 1].toInt().and(255).shl(16)) .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 { public fun MiraiFile.readText(): String {
return input().use { it.readText() } return input().use { it.readAllText() }
} }
public fun MiraiFile.readBytes(): ByteArray { public fun MiraiFile.readBytes(): ByteArray {

View File

@ -133,6 +133,8 @@ public fun Input._readTLVMap(
return map 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 = public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String =
String(this.readBytes(length), charset = charset) // stdlib 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 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 package net.mamoe.mirai.utils
public actual inline fun measureTimeMillis(block: () -> Unit): Long { 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 package net.mamoe.mirai.utils
internal actual fun hash(e: Throwable): Long { internal actual fun hash(e: Throwable): Long {
// Stacktrace analysis not available
var hashCode = 1L var hashCode = 1L
for (stackTraceAddress in e.getStackTraceAddresses()) { val trace = e.getStackTraceAddresses()
for (stackTraceAddress in trace) {
hashCode = (hashCode xor stackTraceAddress).shl(1) 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) @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) 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 { lock.withLock {
return registered[baseClass]?.map { it.instance } return registered[baseClass]?.map { it.instance }
} }
} }
public fun print(): String {
lock.withLock {
return registered.entries.joinToString { "${it.key}:${it.value}" }
}
}
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -57,10 +63,10 @@ public actual fun <T : Any> loadService(
clazz: KClass<out T>, clazz: KClass<out T>,
fallbackImplementation: String? fallbackImplementation: String?
): T = loadServiceOrNull(clazz, fallbackImplementation) ): 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> = 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>) = private fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>) =
clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz") clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")

View File

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

View File

@ -9,10 +9,12 @@
package net.mamoe.mirai.utils package net.mamoe.mirai.utils
private val properties: MutableMap<String, String> = ConcurrentHashMap()
internal actual fun getProperty(name: String, default: String): String? { 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) { 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 @AfterTest
fun afterTest() { fun afterTest() {
println("Cleaning up...") println("Cleaning up...")
baseTempDir.deleteRecursively() println("deleteRecursively:" + baseTempDir.deleteRecursively())
} }
@BeforeTest @BeforeTest
@ -35,8 +35,8 @@ internal abstract class AbstractNativeMiraiFileImplTest {
@Test @Test
fun `canonical paths for canonical input`() { fun `canonical paths for canonical input`() {
assertEquals(tempPath, tempDir.path) assertPathEquals(tempPath, tempDir.path)
assertEquals(tempPath, tempDir.absolutePath) assertPathEquals(tempPath, tempDir.absolutePath)
} }
@Test @Test
@ -46,31 +46,26 @@ internal abstract class AbstractNativeMiraiFileImplTest {
} }
@Test @Test
fun `canonical paths for non-canonical input`() { open fun `canonical paths for non-canonical input`() {
// extra / // extra /
MiraiFile.create("$tempPath/").resolve("test").let { MiraiFile.create("$tempPath/").resolve("test").let {
assertEquals("${tempPath}/test", it.path) assertPathEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath) assertPathEquals("${tempPath}/test", it.absolutePath)
} }
// extra // // extra //
MiraiFile.create("$tempPath//").resolve("test").let { MiraiFile.create("$tempPath//").resolve("test").let {
assertEquals("${tempPath}/test", it.path) assertPathEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath) assertPathEquals("${tempPath}/test", it.absolutePath)
} }
// extra /. // extra /.
MiraiFile.create("$tempPath/.").resolve("test").let { MiraiFile.create("$tempPath/.").resolve("test").let {
assertEquals("${tempPath}/test", it.path) assertPathEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath) assertPathEquals("${tempPath}/test", it.absolutePath)
} }
// extra /./. // extra /./.
MiraiFile.create("$tempPath/./.").resolve("test").let { MiraiFile.create("$tempPath/./.").resolve("test").let {
assertEquals("${tempPath}/test", it.path) assertPathEquals("${tempPath}/test", it.path)
assertEquals("${tempPath}/test", it.absolutePath) assertPathEquals("${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)
} }
} }
@ -90,7 +85,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
assertFalse { tempDir.resolve("not_existing_dir").exists() } assertFalse { tempDir.resolve("not_existing_dir").exists() }
assertEquals(0L, tempDir.resolve("not_existing_dir").length) assertEquals(0L, tempDir.resolve("not_existing_dir").length)
assertTrue { tempDir.resolve("not_existing_dir").mkdir() } 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() } assertTrue { tempDir.resolve("not_existing_dir").exists() }
} }
@ -98,20 +93,27 @@ internal abstract class AbstractNativeMiraiFileImplTest {
fun `isFile isDirectory`() { fun `isFile isDirectory`() {
assertTrue { tempDir.exists() } assertTrue { tempDir.exists() }
println("1")
assertFalse { tempDir.resolve("not_existing_file.txt").exists() } assertFalse { tempDir.resolve("not_existing_file.txt").exists() }
assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile) assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile)
println("1")
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() } assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() }
assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile) assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile)
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory) assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_file.txt").exists() } assertTrue { tempDir.resolve("not_existing_file.txt").exists() }
println("1")
assertFalse { tempDir.resolve("not_existing_dir").exists() } assertFalse { tempDir.resolve("not_existing_dir").exists() }
assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory) assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_dir").mkdir() } assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
assertEquals(false, tempDir.resolve("not_existing_dir").isFile) assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory) assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory)
println("1")
assertTrue { tempDir.resolve("not_existing_dir").exists() } assertTrue { tempDir.resolve("not_existing_dir").exists() }
} }
@ -134,7 +136,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
@Test @Test
fun readText() { fun readText() {
tempDir.resolve("readText1.txt").let { file -> tempDir.resolve("readText2.txt").let { file ->
assertTrue { !file.exists() } assertTrue { !file.exists() }
assertFailsWith<IOException> { file.readText() } assertFailsWith<IOException> { file.readText() }
@ -143,4 +145,39 @@ internal abstract class AbstractNativeMiraiFileImplTest {
assertEquals(text, file.readText()) 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 @Test
fun `can get currentTimeMillis`() { fun `can get currentTimeMillis`() {
val time = currentTimeMillis() val time = currentTimeMillis()
assertTrue(time.toString()) { time > 1642549113 } assertTrue(time.toString()) { time > 1654209523269 }
} }
@Test @Test

View File

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

View File

@ -28,6 +28,16 @@ internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
super.parent() 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 @Test
override fun `resolve absolute`() { override fun `resolve absolute`() {
MiraiFile.create("$tempPath/").resolve("/Users").let { 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") @file:Suppress("UNUSED_VARIABLE")
import BinaryCompatibilityConfigurator.configureBinaryValidators import BinaryCompatibilityConfigurator.configureBinaryValidators
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins { plugins {
kotlin("multiplatform") kotlin("multiplatform")
@ -57,6 +58,8 @@ kotlin {
implementation(bouncycastle) implementation(bouncycastle)
implementation(`log4j-api`) implementation(`log4j-api`)
implementation(`netty-all`) implementation(`netty-all`)
implementation(`ktor-client-okhttp`)
api(`kotlinx-coroutines-core`)
} }
} }
@ -102,6 +105,55 @@ kotlin {
dependencies { 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 * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmName("MiraiImplKt_common")
package net.mamoe.mirai.internal package net.mamoe.mirai.internal
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.features.* import io.ktor.client.features.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* 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.contact.info.StrangerInfoImpl.Companion.impl
import net.mamoe.mirai.internal.event.EventChannelToEventDispatcherAdapter import net.mamoe.mirai.internal.event.EventChannelToEventDispatcherAdapter
import net.mamoe.mirai.internal.event.InternalEventMechanism 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.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.*
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.*
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.*
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromFriendImpl import net.mamoe.mirai.internal.message.toMessageChainNoSource
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.network.components.EventDispatcher import net.mamoe.mirai.internal.network.components.EventDispatcher
import net.mamoe.mirai.internal.network.highway.ChannelKind import net.mamoe.mirai.internal.network.highway.ChannelKind
import net.mamoe.mirai.internal.network.highway.ResourceKind 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.action.Nudge
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.*
import kotlin.jvm.JvmName
internal fun getMiraiImpl() = Mirai as MiraiImpl internal fun getMiraiImpl() = Mirai as MiraiImpl
internal expect fun createDefaultHttpClient(): HttpClient
@Suppress("FunctionName")
internal expect fun _MiraiImpl_static_init()
@OptIn(LowLevelApi::class) @OptIn(LowLevelApi::class)
// not object for ServiceLoader. // not object for ServiceLoader.
internal open class MiraiImpl : IMirai, LowLevelApiAccessor { internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
companion object { companion object {
init { init {
_MiraiImpl_static_init()
MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer()) MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())
MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer()) MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())
MessageSerializers.registerSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.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 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) @Deprecated("Mirai is not going to use ktor. This is deprecated for removal.", level = DeprecationLevel.WARNING)
override var Http: HttpClient = HttpClient() { override var Http: HttpClient = createDefaultHttpClient()
install(HttpTimeout) {
this.requestTimeoutMillis = 30_0000
this.connectTimeoutMillis = 30_0000
this.socketTimeoutMillis = 30_0000
}
}
override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) { override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") @Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
@ -870,17 +863,21 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
return main.toForwardMessageNodes(bot, context) 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 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( return ForwardMessage.Node(
senderId = msg.msgHead.fromUin, senderId = msg.msgHead.fromUin,
time = msg.msgHead.msgTime, time = msg.msgHead.msgTime,
senderName = msg.msgHead.groupInfo?.groupCard senderName = senderName,
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() } messageChain = chain
?: msg.msgHead.fromUin.toString(),
messageChain = listOf(msg)
.toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
.refineDeep(bot, refineContext)
) )
} }

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

View File

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

View File

@ -10,6 +10,7 @@
package net.mamoe.mirai.internal.network.components package net.mamoe.mirai.internal.network.components
import io.ktor.client.request.* import io.ktor.client.request.*
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json 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.internal.utils.crypto.defaultInitialPublicKey
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.currentTimeSeconds 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.") logger.info("ECDH key is invalid, start to fetch ecdh public key from server.")
val respStr = val respStr =
@Suppress("DEPRECATION", "DEPRECATION_ERROR") @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) val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr)
resp.pubKeyMeta.let { meta -> resp.pubKeyMeta.let { meta ->
val isValid = ECDH.verifyPublicKey( 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.component.ComponentKey
import net.mamoe.mirai.internal.network.components.ServerList.Companion.DEFAULT_SERVER_LIST 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.SocketAddress
import net.mamoe.mirai.internal.network.handler.createSocketAddress
import net.mamoe.mirai.utils.MiraiLogger import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.TestOnly import net.mamoe.mirai.utils.TestOnly
import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.info
@ -33,7 +34,7 @@ internal data class ServerAddress(
return "$host:$port" 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 io.ktor.utils.io.core.*
import kotlinx.coroutines.* 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.components.*
import net.mamoe.mirai.internal.network.handler.selector.NetworkException import net.mamoe.mirai.internal.network.handler.selector.NetworkException
import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector
@ -89,14 +91,67 @@ internal abstract class CommonNetworkHandler<Conn>(
internal inner class PacketDecodePipeline(parentContext: CoroutineContext) : internal inner class PacketDecodePipeline(parentContext: CoroutineContext) :
CoroutineScope by parentContext.childScope() { CoroutineScope by parentContext.childScope() {
private val packetCodec: PacketCodec by lazy { context[PacketCodec] } 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 { launch {
packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" } while (isActive) {
val result = packetCodec.processBody(context.bot, raw) val result = queue.receiveCatching()
if (result == null) { packetLogger.verbose { "Decoding packet: $result" }
collectUnknownPacket(raw) result.onFailure { if (it is CancellationException) return@launch }
} else collectReceived(result)
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()) } this.setState { StateConnecting(ExceptionCollector()) }
?.resumeConnection() ?.resumeConnection()
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread. ?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread.
println("INITIALIZED RETURN")
} }
override fun toString(): String = "StateInitialized" override fun toString(): String = "StateInitialized"
@ -214,9 +267,6 @@ internal abstract class CommonNetworkHandler<Conn>(
connectResult.await() // propagates exceptions connectResult.await() // propagates exceptions
val connection = connection.await() val connection = connection.await()
this.setState { StateLoading(connection) } this.setState { StateLoading(connection) }
.also {
println(" this.setState { StateLoading(connection) }: " + it)
}
?.resumeConnection() ?.resumeConnection()
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread. ?: 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 { internal expect abstract class SocketAddress
val host: String
val port: Int
}
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.debug
import net.mamoe.mirai.utils.systemProp import net.mamoe.mirai.utils.systemProp
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
import kotlin.native.concurrent.ThreadLocal
internal class LoggingStateObserver( internal class LoggingStateObserver(
val logger: MiraiLogger, val logger: MiraiLogger,
@ -72,6 +73,7 @@ internal class LoggingStateObserver(
) )
} }
@ThreadLocal
companion object { companion object {
/** /**
* - `on`/`true` for simple logging * - `on`/`true` for simple logging

View File

@ -422,5 +422,5 @@ internal fun String.toIpV4Long(): Long {
if (isEmpty()) return 0 if (isEmpty()) return 0
val split = split('.') val split = split('.')
if (split.size != 4) return 0 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 package net.mamoe.mirai.internal.pipeline
import io.ktor.util.collections.*
import io.ktor.utils.io.core.* import io.ktor.utils.io.core.*
import net.mamoe.mirai.internal.message.contextualBugReportException import net.mamoe.mirai.internal.message.contextualBugReportException
import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext 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 * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:JvmName("PlatformSocketKt_common")
package net.mamoe.mirai.internal.utils package net.mamoe.mirai.internal.utils
import io.ktor.utils.io.core.* import io.ktor.utils.io.core.*
import io.ktor.utils.io.errors.* 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 net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel
import kotlin.jvm.JvmName
/** /**
* TCP Socket. * TCP Socket.
@ -32,7 +38,6 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel {
* @throws ReadPacketInternalException * @throws ReadPacketInternalException
*/ */
override suspend fun read(): ByteReadPacket override suspend fun read(): ByteReadPacket
suspend fun connect(serverHost: String, serverPort: Int)
companion object { companion object {
suspend fun connect( 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 { internal expect class SocketException : IOException {
constructor() 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 ECDHPrivateKey
internal expect interface ECDHPublicKey { internal expect interface ECDHPublicKey
fun getEncoded(): ByteArray
}
internal expect class ECDHKeyPairImpl : ECDHKeyPair internal expect class ECDHKeyPairImpl : ECDHKeyPair
@ -48,9 +46,6 @@ internal interface ECDHKeyPair {
} }
} }
/**
* 椭圆曲线密码, ECDH 加密
*/
internal expect class ECDH(keyPair: ECDHKeyPair) { internal expect class ECDH(keyPair: ECDHKeyPair) {
val keyPair: ECDHKeyPair val keyPair: ECDHKeyPair
@ -62,8 +57,11 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
companion object { companion object {
val isECDHAvailable: Boolean 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 fun constructPublicKey(key: ByteArray): ECDHPublicKey
@ -78,7 +76,7 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
/** /**
* 由一对密匙计算 shareKey * 由一对密匙计算服务器需要的 shareKey
*/ */
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
} }
@ -119,19 +117,12 @@ internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialP
@Serializable @Serializable
internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) { internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
@Transient @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") } 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.CoroutineStart
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.event.GlobalEventChannel import net.mamoe.mirai.event.GlobalEventChannel
import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.internal.test.runBlockingUnit import net.mamoe.mirai.internal.test.runBlockingUnit
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertIs import kotlin.test.assertIs
@JvmBlockingBridge
internal class EventChannelFlowTest : AbstractEventTest() { internal class EventChannelFlowTest : AbstractEventTest() {
@Test @Test

View File

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

View File

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

View File

@ -21,21 +21,22 @@ internal class MessageProtocolFacadeTest : AbstractTest() {
assertEquals( assertEquals(
""" """
QuoteReplyProtocol QuoteReplyProtocol
AudioProtocol
CustomMessageProtocol CustomMessageProtocol
FaceProtocol
FileMessageProtocol FileMessageProtocol
FlashImageProtocol FlashImageProtocol
FaceProtocol
ImageProtocol ImageProtocol
MarketFaceProtocol MarketFaceProtocol
MusicShareProtocol MusicShareProtocol
PokeMessageProtocol PokeMessageProtocol
IgnoredMessagesProtocol
PttMessageProtocol PttMessageProtocol
RichMessageProtocol RichMessageProtocol
TextProtocol TextProtocol
VipFaceProtocol VipFaceProtocol
ForwardMessageProtocol ForwardMessageProtocol
LongMessageProtocol LongMessageProtocol
IgnoredMessagesProtocol
UnsupportedMessageProtocol UnsupportedMessageProtocol
GeneralMessageSenderProtocol GeneralMessageSenderProtocol
""".trimIndent(), """.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.PlainText
import net.mamoe.mirai.message.data.QuoteReply import net.mamoe.mirai.message.data.QuoteReply
import net.mamoe.mirai.message.data.messageChainOf import net.mamoe.mirai.message.data.messageChainOf
import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.utils.hexToBytes import net.mamoe.mirai.utils.hexToBytes
import kotlin.test.Test 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' // 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' // mirai's OfflineMessageSource has no enough information to create 'srcMsg'
), ),
), ),

View File

@ -14,6 +14,19 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
internal abstract class AbstractMutableComponentStorageTest : AbstractTest() { 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 protected abstract fun createStorage(): MutableComponentStorage
@Test @Test

View File

@ -7,15 +7,30 @@
* https://github.com/mamoe/mirai/blob/dev/LICENSE * https://github.com/mamoe/mirai/blob/dev/LICENSE
*/ */
@file:OptIn(TestOnly::class)
package net.mamoe.mirai.internal.network.component package net.mamoe.mirai.internal.network.component
import net.mamoe.mirai.internal.test.AbstractTest import net.mamoe.mirai.internal.test.AbstractTest
import net.mamoe.mirai.utils.TestOnly
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertSame import kotlin.test.assertSame
internal class CombinedStorageTest : AbstractTest() { 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 @Test
fun `can get from main`() { fun `can get from main`() {
val storage = ConcurrentComponentStorage().apply { 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.component.setAll
import net.mamoe.mirai.internal.network.components.* import net.mamoe.mirai.internal.network.components.*
import net.mamoe.mirai.internal.network.framework.components.TestSsoProcessor 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.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.data.jce.SvcRespRegister
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
import net.mamoe.mirai.internal.utils.subLogger 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 //Use overrideComponents to avoid StackOverflowError when applying components
open fun createAddress(): SocketAddress = open fun createAddress(): SocketAddress =
overrideComponents[ServerList].pollAny().let { SocketAddress(it.host, it.port) } overrideComponents[ServerList].pollAny().let { createSocketAddress(it.host, it.port) }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Assertions // Assertions

View File

@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.utils.TestOnly import net.mamoe.mirai.utils.TestOnly
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertFails import kotlin.test.assertFails
import kotlin.test.assertTrue
/** /**
* Test whether the selector can recover the connection after first successful login. * 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. bot.components[EventDispatcher].joinBroadcast() // Wait our async connector to complete.
// BotOfflineMonitor immediately launches a recovery which is UNDISPATCHED, so connection is immediately recovered. // 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 @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.event.events.GroupTempMessageEvent
import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC
import net.mamoe.mirai.internal.test.runBlockingUnit import net.mamoe.mirai.internal.test.runBlockingUnit
import net.mamoe.mirai.message.data.MessageSource import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.OnlineMessageSource
import net.mamoe.mirai.message.data.PlainText
import net.mamoe.mirai.message.data.content
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -124,7 +121,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time) assertEquals(1630, time)
assertEquals(1230001, fromId) assertEquals(1230001, fromId)
assertEquals(2230203, targetId) assertEquals(2230203, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
} }
assertIs<PlainText>(get(1)) assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content) assertEquals("hello", get(1).content)
@ -205,7 +202,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time) assertEquals(1630, time)
assertEquals(1230001, fromId) assertEquals(1230001, fromId)
assertEquals(1230003, targetId) assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
} }
assertIs<PlainText>(get(1)) assertIs<PlainText>(get(1))
assertEquals("123", get(1).content) assertEquals("123", get(1).content)
@ -298,7 +295,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time) assertEquals(1630, time)
assertEquals(1230001, fromId) assertEquals(1230001, fromId)
assertEquals(1230003, targetId) assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
} }
assertIs<PlainText>(get(1)) assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content) assertEquals("hello", get(1).content)
@ -392,7 +389,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
assertEquals(1630, time) assertEquals(1630, time)
assertEquals(1230001, fromId) assertEquals(1230001, fromId)
assertEquals(1230003, targetId) assertEquals(1230003, targetId)
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage) assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
} }
assertIs<PlainText>(get(1)) assertIs<PlainText>(get(1))
assertEquals("hello", get(1).content) 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 import java.net.InetSocketAddress
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility @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) return InetSocketAddress.createUnresolved(host, port)
} }

View File

@ -9,6 +9,7 @@
package net.mamoe.mirai.internal.network.impl.netty package net.mamoe.mirai.internal.network.impl.netty
import io.ktor.utils.io.core.*
import io.netty.bootstrap.Bootstrap import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.channel.* import io.netty.channel.*
@ -20,13 +21,11 @@ import io.netty.handler.codec.MessageToByteEncoder
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.job 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.CommonNetworkHandler
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.utils.cast
import net.mamoe.mirai.utils.debug import net.mamoe.mirai.utils.debug
import java.net.SocketAddress import java.net.SocketAddress
import io.netty.channel.Channel as NettyChannel import io.netty.channel.Channel as NettyChannel
@ -34,7 +33,7 @@ import io.netty.channel.Channel as NettyChannel
internal open class NettyNetworkHandler( internal open class NettyNetworkHandler(
context: NetworkHandlerContext, context: NetworkHandlerContext,
address: SocketAddress, address: SocketAddress,
) : CommonNetworkHandler<NettyChannel>(context, address) { ) : CommonNetworkHandler<NettyChannel>(context, address.cast()) {
override fun toString(): String { override fun toString(): String {
return "NettyNetworkHandler(context=$context, address=$address)" return "NettyNetworkHandler(context=$context, address=$address)"
} }
@ -52,26 +51,11 @@ internal open class NettyNetworkHandler(
// netty conn. // netty conn.
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) { private inner class IncomingPacketDecoder(
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 val decodePipeline: PacketDecodePipeline, private val decodePipeline: PacketDecodePipeline,
) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) { ) : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) { override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
decodePipeline.send(msg) decodePipeline.send(msg.toReadPacket())
} }
} }
@ -91,8 +75,7 @@ internal open class NettyNetworkHandler(
}) })
.addLast("outgoing-packet-encoder", OutgoingPacketEncoder()) .addLast("outgoing-packet-encoder", OutgoingPacketEncoder())
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4)) .addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4))
.addLast(ByteBufToIncomingPacketDecoder()) .addLast(IncomingPacketDecoder(decodePipeline))
.addLast("raw-packet-collector", RawIncomingPacketCollector(decodePipeline))
} }
protected open fun createDummyDecodePipeline() = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext) 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.NetworkHandlerContext
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory 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> { internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> {
override fun create(context: NetworkHandlerContext, address: SocketAddress): 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) { runInterruptible(Dispatchers.IO) {
socket = Socket(serverHost, serverPort) socket = Socket(serverHost, serverPort)
readChannel = socket.getInputStream().buffered() readChannel = socket.getInputStream().buffered()

View File

@ -11,13 +11,580 @@
package net.mamoe.mirai.internal.utils 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.Contact
import net.mamoe.mirai.contact.Group 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.MessageReceipt
import net.mamoe.mirai.message.data.FileMessage 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.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.RemoteFile import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH
import java.io.File 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( internal actual class RemoteFileImpl actual constructor(
contact: Group, contact: Group,

View File

@ -12,6 +12,7 @@
package net.mamoe.mirai.internal.utils.crypto package net.mamoe.mirai.internal.utils.crypto
import net.mamoe.mirai.utils.decodeBase64 import net.mamoe.mirai.utils.decodeBase64
import net.mamoe.mirai.utils.hexToBytes
import java.security.KeyFactory import java.security.KeyFactory
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey 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") KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64())) .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.Test
import kotlin.test.assertEquals 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 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) override fun createStorage(): MutableComponentStorage = ConcurrentComponentStorage(showAllComponents = true)
@Test @Test

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