mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-27 00:30:17 +08:00
Implement mirai-core for native
This commit is contained in:
parent
92222cf1e0
commit
13dadd5a95
@ -13,6 +13,7 @@ import org.gradle.api.Project
|
||||
import org.gradle.api.attributes.Attribute
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getting
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME
|
||||
@ -85,6 +86,8 @@ val LINUX_TARGETS = setOf("linuxX64")
|
||||
|
||||
val UNIX_LIKE_TARGETS by lazy { LINUX_TARGETS + MAC_TARGETS }
|
||||
|
||||
val NATIVE_TARGETS by lazy { UNIX_LIKE_TARGETS + WIN_TARGETS }
|
||||
|
||||
|
||||
fun Project.configureHMPPJvm() {
|
||||
extensions.getByType(KotlinMultiplatformExtension::class.java).apply {
|
||||
@ -277,6 +280,16 @@ fun KotlinMultiplatformExtension.configureNativeTargetsHierarchical(
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround from https://youtrack.jetbrains.com/issue/KT-52433/KotlinNative-Unable-to-generate-framework-with-Kotlin-1621-and-Xcode-134#focus=Comments-27-6140143.0-0
|
||||
project.tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink>().configureEach {
|
||||
val properties = listOf(
|
||||
"ios_arm32", "watchos_arm32", "watchos_x86"
|
||||
).joinToString(separator = ";") { "clangDebugFlags.$it=-Os" }
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xoverride-konan-properties=$properties"
|
||||
)
|
||||
}
|
||||
|
||||
jvmBaseMain.dependsOn(commonMain)
|
||||
jvmBaseTest.dependsOn(commonTest)
|
||||
|
||||
|
@ -31,7 +31,7 @@ object Versions {
|
||||
const val coroutines = "1.6.2"
|
||||
const val atomicFU = "0.17.2"
|
||||
const val serialization = "1.3.2"
|
||||
const val ktor = "1.6.7"
|
||||
const val ktor = "1.6.8"
|
||||
|
||||
const val binaryValidator = "0.4.0"
|
||||
|
||||
|
@ -23,4 +23,6 @@ gnsp.disableApplyOnlyOnRootProjectEnforcement=true
|
||||
mirai.android.target.api.level=24
|
||||
# Enable if you want to use mavenLocal for both Gradle plugin and project dependencies resolutions.
|
||||
systemProp.use.maven.local=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.caching=true
|
||||
kotlin.native.ignoreIncorrectDependencies=true
|
||||
kotlin.mpp.enableCInteropCommonization=true
|
@ -46,6 +46,7 @@ internal object PluginDataRenameToIdTest : AbstractTestPointAsPlugin() {
|
||||
test: a
|
||||
""".trimIndent()
|
||||
)
|
||||
File("data/PluginDataRenameToIdTest").mkdirs()
|
||||
File("data/PluginDataRenameToIdTest/test.txt").createNewFile()
|
||||
File("data/PluginDataRenameToIdTest/testdata.yml").writeText(
|
||||
"""
|
||||
|
@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir
|
||||
public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
}
|
||||
|
||||
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder {
|
||||
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit {
|
||||
public fun <init> (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
|
||||
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object;
|
||||
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void;
|
||||
public abstract fun default (Lkotlin/jvm/functions/Function3;)V
|
||||
public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope;
|
||||
protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred;
|
||||
public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V
|
||||
public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V
|
||||
public fun reply-8NSq9Eo (JLjava/lang/String;)V
|
||||
public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V
|
||||
public final fun timeout (JLkotlin/jvm/functions/Function1;)V
|
||||
public final fun timeout-1WcQj8o (J)J
|
||||
public final synthetic fun timeout-ncvN2qU (J)J
|
||||
public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V
|
||||
public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker {
|
||||
@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
|
||||
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain {
|
||||
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
|
||||
public synthetic fun add (ILjava/lang/Object;)V
|
||||
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
|
||||
public synthetic fun add (Ljava/lang/Object;)Z
|
||||
public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
|
||||
public fun addAll (ILjava/util/Collection;)Z
|
||||
public fun addAll (Ljava/util/Collection;)Z
|
||||
public fun clear ()V
|
||||
public final fun contains (Ljava/lang/Object;)Z
|
||||
public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
|
||||
public fun containsAll (Ljava/util/Collection;)Z
|
||||
public fun contentToString ()Ljava/lang/String;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public synthetic fun get (I)Ljava/lang/Object;
|
||||
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public fun getHasConstrainSingle ()Z
|
||||
public fun getSize ()I
|
||||
public fun hashCode ()I
|
||||
public final fun indexOf (Ljava/lang/Object;)I
|
||||
public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
|
||||
public fun isEmpty ()Z
|
||||
public fun iterator ()Ljava/util/Iterator;
|
||||
public final fun lastIndexOf (Ljava/lang/Object;)I
|
||||
public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
|
||||
public fun listIterator ()Ljava/util/ListIterator;
|
||||
public fun listIterator (I)Ljava/util/ListIterator;
|
||||
public synthetic fun remove (I)Ljava/lang/Object;
|
||||
public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public fun remove (Ljava/lang/Object;)Z
|
||||
public fun removeAll (Ljava/util/Collection;)Z
|
||||
public fun replaceAll (Ljava/util/function/UnaryOperator;)V
|
||||
public fun retainAll (Ljava/util/Collection;)Z
|
||||
public fun serializeToMiraiCode ()Ljava/lang/String;
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
|
||||
public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public final fun size ()I
|
||||
public fun sort (Ljava/util/Comparator;)V
|
||||
public fun subList (II)Ljava/util/List;
|
||||
public fun toArray ()[Ljava/lang/Object;
|
||||
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
|
||||
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun getCause ()Ljava/lang/Throwable;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
|
||||
@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/
|
||||
public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun error0 (Ljava/lang/String;)V
|
||||
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public final fun info (Ljava/lang/String;)V
|
||||
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun info0 (Ljava/lang/String;)V
|
||||
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun isEnabled ()Z
|
||||
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
|
||||
public final fun verbose (Ljava/lang/String;)V
|
||||
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun verbose0 (Ljava/lang/String;)V
|
||||
@ -6466,7 +6423,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util
|
||||
public fun error (Ljava/lang/String;)V
|
||||
public fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun error (Ljava/lang/Throwable;)V
|
||||
public synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public fun getIdentity ()Ljava/lang/String;
|
||||
public fun info (Ljava/lang/String;)V
|
||||
public fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
@ -6478,7 +6435,7 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util
|
||||
public fun isVerboseEnabled ()Z
|
||||
public fun isWarningEnabled ()Z
|
||||
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
|
||||
public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
|
||||
public fun verbose (Ljava/lang/String;)V
|
||||
public fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun verbose (Ljava/lang/Throwable;)V
|
||||
|
@ -1913,41 +1913,23 @@ public abstract class net/mamoe/mirai/event/MessageSelectBuilder : net/mamoe/mir
|
||||
public synthetic fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
}
|
||||
|
||||
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/MessageSubscribersBuilder {
|
||||
public abstract class net/mamoe/mirai/event/MessageSelectBuilderUnit : net/mamoe/mirai/event/CommonMessageSelectBuilderUnit {
|
||||
public fun <init> (Lnet/mamoe/mirai/event/events/MessageEvent;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V
|
||||
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Object;
|
||||
public synthetic fun always (Lkotlin/jvm/functions/Function3;)Ljava/lang/Void;
|
||||
public abstract fun default (Lkotlin/jvm/functions/Function3;)V
|
||||
public final fun defaultQuoteReply (Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun defaultReply (Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun invoke-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun invoke-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
protected abstract fun obtainCurrentCoroutineScope ()Lkotlinx/coroutines/CoroutineScope;
|
||||
protected abstract fun obtainCurrentDeferred ()Lkotlinx/coroutines/CompletableDeferred;
|
||||
public fun quoteReply-8NSq9Eo (JLjava/lang/String;)V
|
||||
public fun quoteReply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public fun quoteReply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
|
||||
public final synthetic fun quoteReply-sCZ5gAI (JLjava/lang/String;)V
|
||||
public fun reply-8NSq9Eo (JLjava/lang/String;)V
|
||||
public fun reply-8NSq9Eo (JLkotlin/jvm/functions/Function1;)V
|
||||
public fun reply-8NSq9Eo (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-AVDwu3U (JLnet/mamoe/mirai/message/data/Message;)V
|
||||
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-RNyhSv4 (JLkotlin/jvm/functions/Function1;)V
|
||||
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)Ljava/lang/Void;
|
||||
public final synthetic fun reply-sCZ5gAI (JLjava/lang/String;)V
|
||||
public final fun timeout (JLkotlin/jvm/functions/Function1;)V
|
||||
public final fun timeout-1WcQj8o (J)J
|
||||
public final synthetic fun timeout-ncvN2qU (J)J
|
||||
public final fun timeoutException (JLkotlin/jvm/functions/Function0;)V
|
||||
public static synthetic fun timeoutException$default (Lnet/mamoe/mirai/event/MessageSelectBuilderUnit;JLkotlin/jvm/functions/Function0;ILjava/lang/Object;)V
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/event/MessageSelectionTimeoutChecker {
|
||||
@ -3609,48 +3591,24 @@ public final class net/mamoe/mirai/message/data/Dice$Key : net/mamoe/mirai/messa
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/MessageChain, net/mamoe/mirai/message/data/MessageChainImpl {
|
||||
public final class net/mamoe/mirai/message/data/EmptyMessageChain : java/util/List, kotlin/jvm/internal/markers/KMappedMarker, net/mamoe/mirai/message/data/DirectSizeAccess, net/mamoe/mirai/message/data/DirectToStringAccess, net/mamoe/mirai/message/data/MessageChain {
|
||||
public static final field INSTANCE Lnet/mamoe/mirai/message/data/EmptyMessageChain;
|
||||
public synthetic fun add (ILjava/lang/Object;)V
|
||||
public fun add (ILnet/mamoe/mirai/message/data/SingleMessage;)V
|
||||
public synthetic fun add (Ljava/lang/Object;)Z
|
||||
public fun add (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
|
||||
public fun addAll (ILjava/util/Collection;)Z
|
||||
public fun addAll (Ljava/util/Collection;)Z
|
||||
public fun clear ()V
|
||||
public final fun contains (Ljava/lang/Object;)Z
|
||||
public fun contains (Lnet/mamoe/mirai/message/data/SingleMessage;)Z
|
||||
public fun containsAll (Ljava/util/Collection;)Z
|
||||
public fun contentToString ()Ljava/lang/String;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public synthetic fun get (I)Ljava/lang/Object;
|
||||
public fun get (I)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public fun getHasConstrainSingle ()Z
|
||||
public fun getSize ()I
|
||||
public fun hashCode ()I
|
||||
public final fun indexOf (Ljava/lang/Object;)I
|
||||
public fun indexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
|
||||
public fun isEmpty ()Z
|
||||
public fun iterator ()Ljava/util/Iterator;
|
||||
public final fun lastIndexOf (Ljava/lang/Object;)I
|
||||
public fun lastIndexOf (Lnet/mamoe/mirai/message/data/SingleMessage;)I
|
||||
public fun listIterator ()Ljava/util/ListIterator;
|
||||
public fun listIterator (I)Ljava/util/ListIterator;
|
||||
public synthetic fun remove (I)Ljava/lang/Object;
|
||||
public fun remove (I)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public fun remove (Ljava/lang/Object;)Z
|
||||
public fun removeAll (Ljava/util/Collection;)Z
|
||||
public fun replaceAll (Ljava/util/function/UnaryOperator;)V
|
||||
public fun retainAll (Ljava/util/Collection;)Z
|
||||
public fun serializeToMiraiCode ()Ljava/lang/String;
|
||||
public final fun serializer ()Lkotlinx/serialization/KSerializer;
|
||||
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
|
||||
public fun set (ILnet/mamoe/mirai/message/data/SingleMessage;)Lnet/mamoe/mirai/message/data/SingleMessage;
|
||||
public final fun size ()I
|
||||
public fun sort (Ljava/util/Comparator;)V
|
||||
public fun subList (II)Ljava/util/List;
|
||||
public fun toArray ()[Ljava/lang/Object;
|
||||
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
@ -5678,6 +5636,8 @@ public final class net/mamoe/mirai/network/NoStandardInputForCaptchaException :
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/RetryLaterException : net/mamoe/mirai/network/LoginFailedException {
|
||||
public synthetic fun <init> (Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun getCause ()Ljava/lang/Throwable;
|
||||
}
|
||||
|
||||
public final class net/mamoe/mirai/network/UnsupportedSliderCaptchaException : net/mamoe/mirai/network/LoginFailedException {
|
||||
@ -6188,14 +6148,11 @@ public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/
|
||||
public final fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun error0 (Ljava/lang/String;)V
|
||||
protected abstract fun error0 (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public final synthetic fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public final fun info (Ljava/lang/String;)V
|
||||
public final fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun info0 (Ljava/lang/String;)V
|
||||
protected abstract fun info0 (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
public fun isEnabled ()Z
|
||||
public synthetic fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
|
||||
public final synthetic fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
|
||||
public final fun verbose (Ljava/lang/String;)V
|
||||
public final fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
|
||||
protected fun verbose0 (Ljava/lang/String;)V
|
||||
|
125
mirai-core-api/src/androidTest/kotlin/android/util/Log.kt
Normal file
125
mirai-core-api/src/androidTest/kotlin/android/util/Log.kt
Normal 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
|
||||
}
|
@ -7,12 +7,4 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
package net.mamoe.mirai.internal.utils
|
||||
|
||||
import net.mamoe.mirai.contact.Group
|
||||
|
||||
internal actual class RemoteFileImpl actual constructor(contact: Group, path: String) :
|
||||
CommonRemoteFileImpl(contact, path) {
|
||||
|
||||
actual constructor(contact: Group, parent: String, name: String) : this(contact, FileSystem.normalize(parent, name))
|
||||
}
|
||||
package net.mameo.mirai
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019-2021 Mamoe Technologies and contributors.
|
||||
* Copyright 2019-2022 Mamoe Technologies and contributors.
|
||||
*
|
||||
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
|
||||
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
|
||||
@ -11,7 +11,6 @@
|
||||
package net.mamoe.mirai.contact
|
||||
|
||||
import net.mamoe.mirai.contact.file.RemoteFiles
|
||||
import net.mamoe.mirai.utils.DeprecatedSinceMirai
|
||||
import net.mamoe.mirai.utils.NotStableForInheritance
|
||||
|
||||
/**
|
||||
@ -24,17 +23,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
|
||||
* @see RemoteFiles
|
||||
*/
|
||||
@NotStableForInheritance
|
||||
public interface FileSupported : Contact {
|
||||
/**
|
||||
* 文件根目录. 可通过 [net.mamoe.mirai.utils.RemoteFile.listFiles] 获取目录下文件列表.
|
||||
*
|
||||
* @since 2.5
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
public val filesRoot: net.mamoe.mirai.utils.RemoteFile
|
||||
|
||||
public expect interface FileSupported : Contact {
|
||||
/**
|
||||
* 获取远程文件列表 (管理器).
|
||||
*
|
||||
|
@ -7,18 +7,13 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmBlockingBridge
|
||||
@file:Suppress("OVERLOADS_INTERFACE")
|
||||
|
||||
package net.mamoe.mirai.contact.file
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.PermissionDeniedException
|
||||
import net.mamoe.mirai.utils.ExternalResource
|
||||
import net.mamoe.mirai.utils.NotStableForInheritance
|
||||
import net.mamoe.mirai.utils.ProgressionCallback
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
/**
|
||||
* 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响.
|
||||
@ -106,7 +101,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder {
|
||||
/**
|
||||
* 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找.
|
||||
*/
|
||||
@JvmOverloads
|
||||
public suspend fun resolveFileById(
|
||||
id: String,
|
||||
deep: Boolean = false
|
||||
@ -143,7 +137,6 @@ public expect interface AbsoluteFolder : AbsoluteFileFolder {
|
||||
*
|
||||
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
|
||||
*/
|
||||
@JvmOverloads
|
||||
public suspend fun uploadNewFile(
|
||||
filepath: String,
|
||||
content: ExternalResource,
|
||||
|
@ -7,12 +7,9 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmBlockingBridge
|
||||
|
||||
package net.mamoe.mirai.contact.roaming
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSource
|
||||
|
@ -266,16 +266,6 @@ public expect abstract class EventChannel<out BaseEvent : Event> @MiraiInternalA
|
||||
*/
|
||||
public fun exceptionHandler(coroutineExceptionHandler: (exception: Throwable) -> Unit): EventChannel<BaseEvent>
|
||||
|
||||
/**
|
||||
* 创建一个新的 [EventChannel], 该 [EventChannel] 包含 [`this.coroutineContext`][defaultCoroutineContext] 和添加的 [coroutineExceptionHandler]
|
||||
* @see context
|
||||
* @since 2.12
|
||||
*/
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
@kotlin.internal.LowPriorityInOverloadResolution
|
||||
public fun exceptionHandler(coroutineExceptionHandler: Consumer<Throwable>): EventChannel<BaseEvent> {
|
||||
return exceptionHandler { coroutineExceptionHandler.accept(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 [coroutineScope] 作为这个 [EventChannel] 的父作用域.
|
||||
|
@ -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
|
||||
|
@ -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>?
|
||||
}
|
@ -25,7 +25,6 @@ import net.mamoe.mirai.event.AbstractEvent
|
||||
import net.mamoe.mirai.internal.event.VerboseEvent
|
||||
import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
import kotlin.jvm.JvmField
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
@ -83,7 +82,6 @@ public data class NewFriendRequestEvent @MiraiInternalApi public constructor(
|
||||
*/
|
||||
public val fromNick: String,
|
||||
) : BotEvent, Packet, AbstractEvent(), FriendInfoChangeEvent {
|
||||
@JvmField
|
||||
internal val responded: AtomicBoolean = atomic(false)
|
||||
|
||||
/**
|
||||
|
@ -29,7 +29,10 @@ import net.mamoe.mirai.internal.network.Packet
|
||||
import net.mamoe.mirai.utils.DeprecatedSinceMirai
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
import kotlin.jvm.*
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmOverloads
|
||||
import kotlin.jvm.JvmStatic
|
||||
|
||||
/**
|
||||
* 机器人被踢出群或在其他客户端主动退出一个群. 在事件广播前 [Bot.groups] 就已删除这个群.
|
||||
@ -354,7 +357,6 @@ public data class BotInvitedJoinGroupRequestEvent @MiraiInternalApi constructor(
|
||||
*/
|
||||
public val invitor: Friend? get() = this.bot.getFriend(invitorId)
|
||||
|
||||
@JvmField
|
||||
internal val responded: AtomicBoolean = atomic(false)
|
||||
|
||||
@JvmBlockingBridge
|
||||
@ -403,8 +405,6 @@ public data class MemberJoinRequestEvent @MiraiInternalApi constructor(
|
||||
*/
|
||||
public val invitor: NormalMember? by lazy { invitorId?.let { group?.get(it) } }
|
||||
|
||||
@JvmField
|
||||
@PublishedApi
|
||||
internal val responded: AtomicBoolean = atomic(false)
|
||||
|
||||
/**
|
||||
|
@ -14,8 +14,6 @@ package net.mamoe.mirai.event
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.event.events.MessageEvent
|
||||
import net.mamoe.mirai.message.data.Message
|
||||
import net.mamoe.mirai.message.data.MessageSource.Key.quote
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.isContextIdenticalWith
|
||||
import net.mamoe.mirai.message.nextMessage
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
@ -207,294 +205,6 @@ public abstract class MessageSelectBuilder<M : MessageEvent, R> @PublishedApi in
|
||||
override fun ListeningFilter.quoteReply(replier: suspend M.(String) -> Any?): Nothing = error("prohibited")
|
||||
}
|
||||
|
||||
/**
|
||||
* [selectMessagesUnit] 或 [selectMessages] 时的 DSL 构建器.
|
||||
*
|
||||
* 它是特殊化的消息监听 ([EventChannel.subscribeMessages]) DSL
|
||||
*
|
||||
* @see MessageSubscribersBuilder 查看上层 API
|
||||
*/
|
||||
public abstract class MessageSelectBuilderUnit<M : MessageEvent, R> @PublishedApi internal constructor(
|
||||
private val ownerMessagePacket: M,
|
||||
stub: Any?,
|
||||
subscriber: (M.(String) -> Boolean, MessageListener<M, Any?>) -> Unit
|
||||
) : MessageSubscribersBuilder<M, Unit, R, Any?>(stub, subscriber) {
|
||||
/**
|
||||
* 当其他条件都不满足时的默认处理.
|
||||
*/
|
||||
@MessageDsl
|
||||
public abstract fun default(onEvent: MessageListener<M, R>) // 需要后置默认监听器
|
||||
|
||||
@Deprecated("Use `default` instead", level = DeprecationLevel.HIDDEN)
|
||||
override fun always(onEvent: MessageListener<M, Any?>): Nothing = error("prohibited")
|
||||
|
||||
/**
|
||||
* 限制本次 select 的最长等待时间, 当超时后抛出 [TimeoutCancellationException]
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
@MessageDsl
|
||||
public fun timeoutException(
|
||||
timeoutMillis: Long,
|
||||
exception: () -> Throwable = { throw MessageSelectionTimeoutException() }
|
||||
) {
|
||||
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
|
||||
obtainCurrentCoroutineScope().launch {
|
||||
delay(timeoutMillis)
|
||||
val deferred = obtainCurrentDeferred() ?: return@launch
|
||||
if (deferred.isActive && !deferred.isCompleted) {
|
||||
deferred.completeExceptionally(exception())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制本次 select 的最长等待时间, 当超时后执行 [block] 以完成 select
|
||||
*/
|
||||
@MessageDsl
|
||||
public fun timeout(timeoutMillis: Long, block: suspend () -> R) {
|
||||
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
|
||||
obtainCurrentCoroutineScope().launch {
|
||||
delay(timeoutMillis)
|
||||
val deferred = obtainCurrentDeferred() ?: return@launch
|
||||
if (deferred.isActive && !deferred.isCompleted) {
|
||||
deferred.complete(block())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
|
||||
*
|
||||
* @see invoke
|
||||
* @see reply
|
||||
*/
|
||||
@MessageDsl
|
||||
public fun timeout(timeoutMillis: Long): MessageSelectionTimeoutChecker {
|
||||
require(timeoutMillis > 0) { "timeoutMillis must be positive" }
|
||||
return MessageSelectionTimeoutChecker(timeoutMillis)
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个限制本次 select 的最长等待时间的 [Deferred]
|
||||
*
|
||||
* @see Deferred<Unit>.invoke
|
||||
*/
|
||||
@Suppress("unused")
|
||||
public fun MessageSelectionTimeoutChecker.invoke(block: suspend () -> R) {
|
||||
return timeout(this.timeoutMillis, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* 在超时后回复原消息
|
||||
*
|
||||
* 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
|
||||
*
|
||||
* @see timeout
|
||||
* @see quoteReply
|
||||
*/
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.reply(block: suspend () -> Any?) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
executeAndReply(block)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.reply(message: Message) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
ownerMessagePacket.subject.sendMessage(message)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.reply(message: String) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
ownerMessagePacket.subject.sendMessage(message)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在超时后引用回复原消息
|
||||
*
|
||||
* 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
|
||||
*
|
||||
* @see timeout
|
||||
* @see reply
|
||||
*/
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.quoteReply(block: suspend () -> Any?) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
executeAndQuoteReply(block)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: Message) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
public open infix fun MessageSelectionTimeoutChecker.quoteReply(message: String) {
|
||||
return timeout(this.timeoutMillis) {
|
||||
ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + message)
|
||||
Unit as R
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当其他条件都不满足时回复原消息.
|
||||
*
|
||||
* 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
|
||||
*/
|
||||
@MessageDsl
|
||||
public fun defaultReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
|
||||
this@MessageSelectBuilderUnit.executeAndReply(block)
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* 当其他条件都不满足时引用回复原消息.
|
||||
*
|
||||
* 当 [block] 返回值为 [Unit] 时不回复, 为 [Message] 时回复 [Message], 其他将 [toString] 后回复为 [PlainText]
|
||||
*/
|
||||
@MessageDsl
|
||||
public fun defaultQuoteReply(block: suspend () -> Any?): Unit = subscriber({ true }, {
|
||||
this@MessageSelectBuilderUnit.executeAndQuoteReply(block)
|
||||
})
|
||||
|
||||
private suspend inline fun executeAndReply(noinline block: suspend () -> Any?) {
|
||||
when (val result = block()) {
|
||||
Unit -> {
|
||||
|
||||
}
|
||||
is Message -> ownerMessagePacket.subject.sendMessage(result)
|
||||
else -> ownerMessagePacket.subject.sendMessage(result.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun executeAndQuoteReply(noinline block: suspend () -> Any?) {
|
||||
when (val result = block()) {
|
||||
Unit -> {
|
||||
|
||||
}
|
||||
is Message -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result)
|
||||
else -> ownerMessagePacket.subject.sendMessage(ownerMessagePacket.message.quote() + result.toString())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JvmName("timeout-ncvN2qU")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public fun timeout00(timeoutMillis: Long): MessageSelectionTimeoutChecker {
|
||||
return timeout(timeoutMillis)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmName("invoke-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public fun MessageSelectionTimeoutChecker.invoke00(block: suspend () -> R) {
|
||||
return invoke(block)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@JvmName("invoke-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public fun MessageSelectionTimeoutChecker.invoke000(block: suspend () -> R): Nothing? {
|
||||
invoke(block)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmName("reply-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply00(block: suspend () -> Any?) {
|
||||
return reply(block)
|
||||
}
|
||||
|
||||
@JvmName("reply-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply000(block: suspend () -> Any?): Nothing? {
|
||||
reply(block)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmName("reply-sCZ5gAI")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply00(message: String) {
|
||||
return reply(message)
|
||||
}
|
||||
|
||||
@JvmName("reply-sCZ5gAI")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply000(message: String): Nothing? {
|
||||
reply(message)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmName("reply-AVDwu3U")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply00(message: Message) {
|
||||
return reply(message)
|
||||
}
|
||||
|
||||
@JvmName("reply-AVDwu3U")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.reply000(message: Message): Nothing? {
|
||||
reply(message)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@JvmName("quoteReply-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply00(block: suspend () -> Any?) {
|
||||
return reply(block)
|
||||
}
|
||||
|
||||
@JvmName("quoteReply-RNyhSv4")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply000(block: suspend () -> Any?): Nothing? {
|
||||
reply(block)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmName("quoteReply-sCZ5gAI")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: String) {
|
||||
return reply(message)
|
||||
}
|
||||
|
||||
@JvmName("quoteReply-sCZ5gAI")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: String): Nothing? {
|
||||
reply(message)
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmName("quoteReply-AVDwu3U")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply00(message: Message) {
|
||||
return reply(message)
|
||||
}
|
||||
|
||||
@JvmName("quoteReply-AVDwu3U")
|
||||
@Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
public infix fun MessageSelectionTimeoutChecker.quoteReply000(message: Message): Nothing? {
|
||||
reply(message)
|
||||
return null
|
||||
}
|
||||
|
||||
protected abstract fun obtainCurrentCoroutineScope(): CoroutineScope
|
||||
protected abstract fun obtainCurrentDeferred(): CompletableDeferred<R>?
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS")
|
||||
public value class MessageSelectionTimeoutChecker internal constructor(public val timeoutMillis: Long)
|
||||
|
@ -22,9 +22,11 @@ import net.mamoe.mirai.contact.FileSupported
|
||||
import net.mamoe.mirai.contact.file.AbsoluteFile
|
||||
import net.mamoe.mirai.event.events.MessageEvent
|
||||
import net.mamoe.mirai.message.code.CodableMessage
|
||||
import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
|
||||
import net.mamoe.mirai.message.data.visitor.MessageVisitor
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.MiraiInternalApi
|
||||
import net.mamoe.mirai.utils.NotStableForInheritance
|
||||
import net.mamoe.mirai.utils.copy
|
||||
import net.mamoe.mirai.utils.map
|
||||
import kotlin.jvm.JvmMultifileClass
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmStatic
|
||||
@ -46,10 +48,11 @@ import kotlin.jvm.JvmSynthetic
|
||||
* @suppress [FileMessage] 的使用是稳定的, 但自行实现不稳定.
|
||||
*/
|
||||
@Serializable(FileMessage.Serializer::class)
|
||||
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
|
||||
@SerialName(FileMessage.SERIAL_NAME)
|
||||
@NotStableForInheritance
|
||||
@JvmBlockingBridge
|
||||
public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
|
||||
public expect interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
|
||||
/**
|
||||
* 服务器需要的某种 ID.
|
||||
*/
|
||||
@ -70,76 +73,58 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage {
|
||||
*/
|
||||
public val size: Long
|
||||
|
||||
override fun contentToString(): String = "[文件]$name" // orthodox
|
||||
open override fun contentToString(): String
|
||||
|
||||
override fun appendMiraiCodeTo(builder: StringBuilder) {
|
||||
builder.append("[mirai:file:")
|
||||
builder.appendStringAsMiraiCode(id).append(",")
|
||||
builder.append(internalId).append(",")
|
||||
builder.appendStringAsMiraiCode(name).append(",")
|
||||
builder.append(size).append("]")
|
||||
}
|
||||
open override fun appendMiraiCodeTo(builder: StringBuilder)
|
||||
|
||||
/**
|
||||
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? {
|
||||
@Suppress("DEPRECATION")
|
||||
return contact.filesRoot.resolveById(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`.
|
||||
* 获取一个对应的 [AbsoluteFile]. 当目标群或好友不存在这个文件时返回 `null`.
|
||||
*
|
||||
* @since 2.8
|
||||
*/
|
||||
public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile?
|
||||
|
||||
override val key: Key get() = Key
|
||||
open override val key: Key
|
||||
|
||||
@MiraiInternalApi
|
||||
override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R {
|
||||
return visitor.visitFileMessage(this, data)
|
||||
}
|
||||
open override fun <D, R> accept(visitor: MessageVisitor<D, R>, data: D): R
|
||||
|
||||
/**
|
||||
* 注意, baseKey [MessageContent] 不稳定. 未来可能会有变更.
|
||||
*/
|
||||
public companion object Key :
|
||||
AbstractPolymorphicMessageKey<MessageContent, FileMessage>(
|
||||
MessageContent, { it.safeCast() }) {
|
||||
AbstractPolymorphicMessageKey<MessageContent, FileMessage> {
|
||||
|
||||
public const val SERIAL_NAME: String = "FileMessage"
|
||||
@Suppress("CONST_VAL_WITHOUT_INITIALIZER")
|
||||
public const val SERIAL_NAME: String
|
||||
|
||||
/**
|
||||
* 构造 [FileMessage]
|
||||
* @since 2.5
|
||||
*/
|
||||
@JvmStatic
|
||||
public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage =
|
||||
Mirai.createFileMessage(id, internalId, name, size)
|
||||
public fun create(id: String, internalId: Int, name: String, size: Long): FileMessage
|
||||
}
|
||||
|
||||
public object Serializer : KSerializer<FileMessage> by FallbackSerializer(SERIAL_NAME) // not polymorphic
|
||||
public object Serializer : KSerializer<FileMessage> // not polymorphic
|
||||
}
|
||||
|
||||
@MiraiInternalApi
|
||||
private open class FallbackSerializer(serialName: String) : KSerializer<FileMessage> by Delegate.serializer().map(
|
||||
@MiraiInternalApi
|
||||
internal open class FallbackFileMessageSerializer constructor(serialName: String) :
|
||||
KSerializer<FileMessage> by Delegate.serializer().map(
|
||||
Delegate.serializer().descriptor.copy(serialName),
|
||||
serialize = { Delegate(id, internalId, name, size) },
|
||||
deserialize = { Mirai.createFileMessage(id, internalId, name, size) },
|
||||
) {
|
||||
@SerialName(SERIAL_NAME)
|
||||
@Serializable
|
||||
data class Delegate(
|
||||
val id: String,
|
||||
val internalId: Int,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
)
|
||||
}
|
||||
@Suppress("ANNOTATION_ARGUMENT_MUST_BE_CONST")
|
||||
@SerialName(FileMessage.SERIAL_NAME)
|
||||
@Serializable
|
||||
data class Delegate constructor(
|
||||
val id: String,
|
||||
val internalId: Int,
|
||||
val name: String,
|
||||
val size: Long,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -21,7 +21,6 @@ import net.mamoe.mirai.event.EventPriority
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.event.events.MessageEvent
|
||||
import net.mamoe.mirai.event.syncFromEvent
|
||||
import net.mamoe.mirai.event.syncFromEventOrNull
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
@ -74,7 +73,7 @@ public suspend inline fun <reified P : MessageEvent> P.nextMessage(
|
||||
* @param filter 过滤器. 返回非 null 则代表得到了需要的值. [syncFromEvent] 会返回这个值
|
||||
* @return 消息链. 超时时返回 `null`
|
||||
*
|
||||
* @see syncFromEventOrNull 实现原理
|
||||
* @see syncFromEvent
|
||||
*/
|
||||
@JvmSynthetic
|
||||
public suspend inline fun <reified P : MessageEvent> P.nextMessageOrNull(
|
||||
|
@ -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>
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -131,9 +131,11 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder {
|
||||
/**
|
||||
* 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找.
|
||||
*/
|
||||
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
|
||||
@JvmOverloads
|
||||
public actual suspend fun resolveFileById(
|
||||
id: String,
|
||||
deep: Boolean
|
||||
deep: Boolean = false
|
||||
): AbsoluteFile?
|
||||
|
||||
/**
|
||||
@ -187,10 +189,12 @@ public actual interface AbsoluteFolder : AbsoluteFileFolder {
|
||||
*
|
||||
* @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传)
|
||||
*/
|
||||
@Suppress("OVERLOADS_INTERFACE", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
|
||||
@JvmOverloads
|
||||
public actual suspend fun uploadNewFile(
|
||||
filepath: String,
|
||||
content: ExternalResource,
|
||||
callback: ProgressionCallback<AbsoluteFile, Long>?,
|
||||
callback: ProgressionCallback<AbsoluteFile, Long>? = null,
|
||||
): AbsoluteFile
|
||||
|
||||
public actual companion object {
|
||||
|
@ -7,9 +7,12 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmBlockingBridge
|
||||
|
||||
package net.mamoe.mirai.contact.roaming
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.contact.Friend
|
||||
import net.mamoe.mirai.message.data.MessageChain
|
||||
import net.mamoe.mirai.message.data.MessageSource
|
||||
@ -46,10 +49,11 @@ public actual interface RoamingMessages {
|
||||
* @param timeEnd 结束时间, UTC+8 时间戳, 单位为秒. 可以为 [Long.MAX_VALUE], 即表示到可以获取的最晚的消息为止. 低于 [timeStart] 的值将会被看作是 [timeStart] 的值.
|
||||
* @param filter 过滤器.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
|
||||
public actual suspend fun getMessagesIn(
|
||||
timeStart: Long,
|
||||
timeEnd: Long,
|
||||
filter: RoamingMessageFilter?
|
||||
filter: RoamingMessageFilter? = null
|
||||
): Flow<MessageChain>
|
||||
|
||||
/**
|
||||
@ -68,8 +72,9 @@ public actual interface RoamingMessages {
|
||||
*
|
||||
* @param filter 过滤器.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") // Keep JVM ABI
|
||||
public actual suspend fun getAllMessages(
|
||||
filter: RoamingMessageFilter?
|
||||
filter: RoamingMessageFilter? = null
|
||||
): Flow<MessageChain> = getMessagesIn(0, Long.MAX_VALUE, filter)
|
||||
|
||||
/**
|
||||
|
@ -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] 的父作用域.
|
||||
*
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@ import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.event.events.BotEvent
|
||||
import net.mamoe.mirai.utils.DeprecatedSinceMirai
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
@ -17,8 +17,6 @@ import net.mamoe.mirai.utils.DeprecatedSinceMirai
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
|
||||
|
||||
/**
|
@ -16,8 +16,6 @@ import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.utils.DeprecatedSinceMirai
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.jvm.JvmName
|
||||
import kotlin.jvm.JvmSynthetic
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
@ -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
|
||||
}
|
@ -60,26 +60,6 @@ public actual open class BotConfiguration { // open for Java
|
||||
*/
|
||||
public var workingDir: File = File(".")
|
||||
|
||||
/**
|
||||
* Json 序列化器, 使用 'kotlinx.serialization'
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
@Deprecated(
|
||||
"Changing serial format is going to be forbidden. Deprecated for removal. ",
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
@DeprecatedSinceMirai(errorSince = "2.11") // was experimental
|
||||
public var json: Json = kotlin.runCatching {
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
}
|
||||
}.getOrElse {
|
||||
@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
|
||||
Json {}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Coroutines
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -341,7 +321,7 @@ public actual open class BotConfiguration { // open for Java
|
||||
@ConfigurationDsl
|
||||
public actual fun loadDeviceInfoJson(json: String) {
|
||||
deviceInfo = {
|
||||
this.json.decodeFromString(DeviceInfo.serializer(), json)
|
||||
DeviceInfoManager.deserialize(json, Companion.json)
|
||||
}
|
||||
}
|
||||
|
||||
@ -603,7 +583,6 @@ public actual open class BotConfiguration { // open for Java
|
||||
// To structural order
|
||||
new.workingDir = workingDir
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
new.json = json
|
||||
new.parentCoroutineContext = parentCoroutineContext
|
||||
new.heartbeatPeriodMillis = heartbeatPeriodMillis
|
||||
new.heartbeatTimeoutMillis = heartbeatTimeoutMillis
|
||||
@ -646,6 +625,20 @@ public actual open class BotConfiguration { // open for Java
|
||||
@JvmStatic
|
||||
public actual val Default: BotConfiguration = BotConfiguration()
|
||||
|
||||
/**
|
||||
* Json 序列化器, 使用 'kotlinx.serialization'
|
||||
*/
|
||||
internal val json: Json = kotlin.runCatching {
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
prettyPrint = true
|
||||
}
|
||||
}.getOrElse {
|
||||
@Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") // compatible for older versions
|
||||
Json {}
|
||||
}
|
||||
|
||||
internal fun BotConfiguration.getFileBasedDeviceInfoSupplier(file: () -> File): (Bot) -> DeviceInfo {
|
||||
return {
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
|
@ -103,106 +103,106 @@ import java.io.File
|
||||
) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
@NotStableForInheritance
|
||||
public actual interface RemoteFile {
|
||||
public interface RemoteFile {
|
||||
/**
|
||||
* 文件名或目录名.
|
||||
*/
|
||||
public actual val name: String
|
||||
public val name: String
|
||||
|
||||
/**
|
||||
* 文件的 ID. 群文件允许重名, ID 非空时用来区分重名.
|
||||
*/
|
||||
public actual val id: String?
|
||||
public val id: String?
|
||||
|
||||
/**
|
||||
* 标准的绝对路径, 起始字符为 '/'. 如 `/foo/bar.txt`.
|
||||
*
|
||||
* 根目录路径为 [ROOT_PATH]
|
||||
*/
|
||||
public actual val path: String
|
||||
public val path: String
|
||||
|
||||
/**
|
||||
* 获取父目录, 当 [RemoteFile] 表示根目录时返回 `null`
|
||||
*/
|
||||
public actual val parent: RemoteFile?
|
||||
public val parent: RemoteFile?
|
||||
|
||||
/**
|
||||
* 此文件所属的群或好友
|
||||
*/
|
||||
public actual val contact: FileSupported
|
||||
public val contact: FileSupported
|
||||
|
||||
/**
|
||||
* 当 [RemoteFile] 表示一个文件时返回 `true`.
|
||||
*/
|
||||
public actual suspend fun isFile(): Boolean
|
||||
public suspend fun isFile(): Boolean
|
||||
|
||||
/**
|
||||
* 当 [RemoteFile] 表示一个目录时返回 `true`.
|
||||
*/
|
||||
public actual suspend fun isDirectory(): Boolean = !isFile()
|
||||
public suspend fun isDirectory(): Boolean = !isFile()
|
||||
|
||||
/**
|
||||
* 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定.
|
||||
*/
|
||||
public actual suspend fun length(): Long
|
||||
public suspend fun length(): Long
|
||||
|
||||
public actual class FileInfo @MiraiInternalApi actual constructor(
|
||||
public class FileInfo @MiraiInternalApi constructor(
|
||||
/**
|
||||
* 文件或目录名.
|
||||
*/
|
||||
public actual val name: String,
|
||||
public val name: String,
|
||||
/**
|
||||
* 唯一识别标识.
|
||||
*/
|
||||
public actual val id: String,
|
||||
public val id: String,
|
||||
/**
|
||||
* 标准绝对路径.
|
||||
*/
|
||||
public actual val path: String,
|
||||
public val path: String,
|
||||
/**
|
||||
* 文件长度 (大小) bytes, 目录的 [length] 为 0.
|
||||
*/
|
||||
public actual val length: Long,
|
||||
public val length: Long,
|
||||
/**
|
||||
* 下载次数. 目录没有下载次数, 此属性总是 `0`.
|
||||
*/
|
||||
public actual val downloadTimes: Int,
|
||||
public val downloadTimes: Int,
|
||||
/**
|
||||
* 上传者 ID. 目录没有上传者, 此属性总是 `0`.
|
||||
*/
|
||||
public actual val uploaderId: Long,
|
||||
public val uploaderId: Long,
|
||||
/**
|
||||
* 上传的时间. 目录没有上传时间, 此属性总是 `0`.
|
||||
*/
|
||||
public actual val uploadTime: Long,
|
||||
public val uploadTime: Long,
|
||||
/**
|
||||
* 上次修改时间. 时间戳秒.
|
||||
*/
|
||||
public actual val lastModifyTime: Long,
|
||||
public actual val sha1: ByteArray,
|
||||
public actual val md5: ByteArray,
|
||||
public val lastModifyTime: Long,
|
||||
public val sha1: ByteArray,
|
||||
public val md5: ByteArray,
|
||||
) {
|
||||
/**
|
||||
* 根据 [FileInfo.id] 或 [FileInfo.path] 获取到对应的 [RemoteFile].
|
||||
*/
|
||||
public actual suspend fun resolveToFile(contact: FileSupported): RemoteFile =
|
||||
public suspend fun resolveToFile(contact: FileSupported): RemoteFile =
|
||||
contact.filesRoot.resolveById(id) ?: contact.filesRoot.resolve(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `null`.
|
||||
*/
|
||||
public actual suspend fun getInfo(): FileInfo?
|
||||
public suspend fun getInfo(): FileInfo?
|
||||
|
||||
/**
|
||||
* 当文件或目录存在时返回 `true`.
|
||||
*/
|
||||
public actual suspend fun exists(): Boolean
|
||||
public suspend fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* @return [path]
|
||||
*/
|
||||
public actual override fun toString(): String
|
||||
public override fun toString(): String
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// resolve
|
||||
@ -214,7 +214,7 @@ public actual interface RemoteFile {
|
||||
* @param relative 相对路径. 当初始字符为 '/' 时将作为绝对路径解析
|
||||
* @see File.resolve stdlib 内的类似函数
|
||||
*/
|
||||
public actual fun resolve(relative: String): RemoteFile
|
||||
public fun resolve(relative: String): RemoteFile
|
||||
|
||||
/**
|
||||
* 获取该目录的子文件. 不会检查 [RemoteFile] 是否表示一个目录. 返回的 [RemoteFile.id] 将会与 `relative.id` 相同.
|
||||
@ -222,19 +222,20 @@ public actual interface RemoteFile {
|
||||
* @param relative 相对路径. 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
|
||||
* @see File.resolve stdlib 内的类似函数
|
||||
*/
|
||||
public actual fun resolve(relative: RemoteFile): RemoteFile
|
||||
public fun resolve(relative: RemoteFile): RemoteFile
|
||||
|
||||
/**
|
||||
* 获取该目录下的 ID 为 [id] 的文件, 当 [deep] 为 `true` 时还会进入子目录继续寻找这样的文件. 在不存在时返回 `null`.
|
||||
* @see resolve
|
||||
*/
|
||||
public actual suspend fun resolveById(id: String, deep: Boolean): RemoteFile?
|
||||
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS") // JVM ABI
|
||||
public suspend fun resolveById(id: String, deep: Boolean = true): RemoteFile?
|
||||
|
||||
/**
|
||||
* 获取该目录或子目录下的 ID 为 [id] 的文件, 在不存在时返回 `null`
|
||||
* @see resolve
|
||||
*/
|
||||
public actual suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
|
||||
public suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
|
||||
|
||||
/**
|
||||
* 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
|
||||
@ -243,7 +244,7 @@ public actual interface RemoteFile {
|
||||
* @param relative 当初始字符为 '/' 时将作为绝对路径解析
|
||||
* @see File.resolveSibling stdlib 内的类似函数
|
||||
*/
|
||||
public actual fun resolveSibling(relative: String): RemoteFile
|
||||
public fun resolveSibling(relative: String): RemoteFile
|
||||
|
||||
/**
|
||||
* 获取父目录的子文件. 如 `RemoteFile("/foo/bar").resolveSibling("gav")` 为 `RemoteFile("/foo/gav")`.
|
||||
@ -252,7 +253,7 @@ public actual interface RemoteFile {
|
||||
* @param relative 当 [RemoteFile.path] 初始字符为 '/' 时将作为绝对路径解析
|
||||
* @see File.resolveSibling stdlib 内的类似函数
|
||||
*/
|
||||
public actual fun resolveSibling(relative: RemoteFile): RemoteFile
|
||||
public fun resolveSibling(relative: RemoteFile): RemoteFile
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// operations
|
||||
@ -261,7 +262,7 @@ public actual interface RemoteFile {
|
||||
/**
|
||||
* 删除这个文件或目录. 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
|
||||
*/
|
||||
public actual suspend fun delete(): Boolean
|
||||
public suspend fun delete(): Boolean
|
||||
|
||||
/**
|
||||
* 重命名这个文件或目录, 将会更改 [RemoteFile.name] 属性值.
|
||||
@ -269,7 +270,7 @@ public actual interface RemoteFile {
|
||||
*
|
||||
* [renameTo] 只会操作远程文件, 而不会修改当前 [RemoteFile.path].
|
||||
*/
|
||||
public actual suspend fun renameTo(name: String): Boolean
|
||||
public suspend fun renameTo(name: String): Boolean
|
||||
|
||||
/**
|
||||
* 将这个目录或文件移动到 [target] 位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
|
||||
@ -287,7 +288,7 @@ public actual interface RemoteFile {
|
||||
*
|
||||
* @param target 目标文件位置.
|
||||
*/
|
||||
public actual suspend fun moveTo(target: RemoteFile): Boolean
|
||||
public suspend fun moveTo(target: RemoteFile): Boolean
|
||||
|
||||
/**
|
||||
* 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`.
|
||||
@ -307,7 +308,7 @@ public actual interface RemoteFile {
|
||||
level = DeprecationLevel.ERROR
|
||||
) // deprecated since 2.7
|
||||
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10")
|
||||
public actual suspend fun moveTo(path: String): Boolean {
|
||||
public suspend fun moveTo(path: String): Boolean {
|
||||
// Impl notes:
|
||||
// if `path` is absolute, this works as intended.
|
||||
// if not, `resolve(path)` will be a child path from this dir and fails always.
|
||||
@ -320,31 +321,31 @@ public actual interface RemoteFile {
|
||||
* 创建后 [isDirectory] 也不一定会返回 `true`.
|
||||
* 当 [id] 未指定时, [RemoteFile] 总是表示一个路径而无法确定目标是文件还是目录, [isFile] 或 [isDirectory] 结果取决于服务器.
|
||||
*/
|
||||
public actual suspend fun mkdir(): Boolean
|
||||
public suspend fun mkdir(): Boolean
|
||||
|
||||
/**
|
||||
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow].
|
||||
*
|
||||
* 返回的 [Flow] 是*冷*的, 只会在被需要的时候向服务器查询.
|
||||
*/
|
||||
public actual suspend fun listFiles(): Flow<RemoteFile>
|
||||
public suspend fun listFiles(): Flow<RemoteFile>
|
||||
|
||||
/**
|
||||
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器.
|
||||
* @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表.
|
||||
*/
|
||||
@JavaFriendlyAPI
|
||||
public actual suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
|
||||
public suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
|
||||
|
||||
/**
|
||||
* 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyList].
|
||||
*/
|
||||
public actual suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
|
||||
public suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
|
||||
|
||||
/**
|
||||
* 得到相应文件消息. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`.
|
||||
*/
|
||||
public actual suspend fun toMessage(): FileMessage?
|
||||
public suspend fun toMessage(): FileMessage?
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// upload & download
|
||||
@ -360,30 +361,30 @@ public actual interface RemoteFile {
|
||||
level = DeprecationLevel.WARNING
|
||||
) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
public actual interface ProgressionCallback {
|
||||
public interface ProgressionCallback {
|
||||
/**
|
||||
* 当上传开始时调用
|
||||
*/
|
||||
public actual fun onBegin(file: RemoteFile, resource: ExternalResource) {}
|
||||
public fun onBegin(file: RemoteFile, resource: ExternalResource) {}
|
||||
|
||||
/**
|
||||
* 每当有进度更新时调用. 此方法可能会同时被多个线程调用.
|
||||
*
|
||||
* 提示: 可通过 [ExternalResource.size] 获取文件总大小.
|
||||
*/
|
||||
public actual fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
|
||||
public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {}
|
||||
|
||||
/**
|
||||
* 当上传成功时调用
|
||||
*/
|
||||
public actual fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
|
||||
public fun onSuccess(file: RemoteFile, resource: ExternalResource) {}
|
||||
|
||||
/**
|
||||
* 当上传以异常失败时调用
|
||||
*/
|
||||
public actual fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
|
||||
public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {}
|
||||
|
||||
public actual companion object {
|
||||
public companion object {
|
||||
/**
|
||||
* 将一个 [SendChannel] 作为 [ProgressionCallback] 使用.
|
||||
*
|
||||
@ -410,7 +411,7 @@ public actual interface RemoteFile {
|
||||
* 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作.
|
||||
*/
|
||||
@JvmStatic
|
||||
public actual fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean): ProgressionCallback {
|
||||
public fun SendChannel<Long>.asProgressionCallback(closeOnFinish: Boolean = false): ProgressionCallback {
|
||||
return object : ProgressionCallback {
|
||||
override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {
|
||||
trySend(downloadedSize)
|
||||
@ -462,9 +463,10 @@ public actual interface RemoteFile {
|
||||
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource, callback)"), DeprecationLevel.ERROR
|
||||
) // deprecated since 2.7-M1
|
||||
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
|
||||
public actual suspend fun upload(
|
||||
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public suspend fun upload(
|
||||
resource: ExternalResource,
|
||||
callback: ProgressionCallback?,
|
||||
callback: ProgressionCallback? = null,
|
||||
): FileMessage
|
||||
|
||||
/**
|
||||
@ -478,7 +480,7 @@ public actual interface RemoteFile {
|
||||
"Use uploadAndSend instead.", ReplaceWith("this.uploadAndSend(resource)"), DeprecationLevel.ERROR
|
||||
) // deprecated since 2.7-M1
|
||||
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
|
||||
public actual suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
|
||||
public suspend fun upload(resource: ExternalResource): FileMessage = upload(resource, null)
|
||||
|
||||
/**
|
||||
* 上传文件.
|
||||
@ -520,7 +522,7 @@ public actual interface RemoteFile {
|
||||
* @see upload
|
||||
*/
|
||||
@MiraiExperimentalApi
|
||||
public actual suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
|
||||
public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact>
|
||||
|
||||
/**
|
||||
* 上传文件并发送文件消息.
|
||||
@ -533,41 +535,41 @@ public actual interface RemoteFile {
|
||||
/**
|
||||
* 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null`
|
||||
*/
|
||||
public actual suspend fun getDownloadInfo(): DownloadInfo?
|
||||
public suspend fun getDownloadInfo(): DownloadInfo?
|
||||
|
||||
public actual class DownloadInfo @MiraiInternalApi actual constructor(
|
||||
public class DownloadInfo @MiraiInternalApi constructor(
|
||||
/**
|
||||
* @see RemoteFile.name
|
||||
*/
|
||||
public actual val filename: String,
|
||||
public val filename: String,
|
||||
/**
|
||||
* @see RemoteFile.id
|
||||
*/
|
||||
public actual val id: String,
|
||||
public val id: String,
|
||||
/**
|
||||
* 标准绝对路径
|
||||
* @see RemoteFile.path
|
||||
*/
|
||||
public actual val path: String,
|
||||
public val path: String,
|
||||
/**
|
||||
* HTTP or HTTPS URL
|
||||
*/
|
||||
public actual val url: String,
|
||||
public actual val sha1: ByteArray,
|
||||
public actual val md5: ByteArray,
|
||||
public val url: String,
|
||||
public val sha1: ByteArray,
|
||||
public val md5: ByteArray,
|
||||
) {
|
||||
actual override fun toString(): String {
|
||||
override fun toString(): String {
|
||||
return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " +
|
||||
"md5=${md5.toUHexString("")})"
|
||||
}
|
||||
}
|
||||
|
||||
public actual companion object {
|
||||
public companion object {
|
||||
/**
|
||||
* 根目录路径
|
||||
* @see RemoteFile.path
|
||||
*/
|
||||
public actual const val ROOT_PATH: String = "/"
|
||||
public const val ROOT_PATH: String = "/"
|
||||
|
||||
/**
|
||||
* 上传文件并获取文件消息, 但不发送.
|
||||
@ -590,10 +592,11 @@ public actual interface RemoteFile {
|
||||
level = DeprecationLevel.ERROR
|
||||
) // deprecated since 2.7-M1
|
||||
@DeprecatedSinceMirai(warningSince = "2.7", errorSince = "2.10") // left ERROR intentionally
|
||||
public actual suspend fun FileSupported.uploadFile(
|
||||
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public suspend fun FileSupported.uploadFile(
|
||||
path: String,
|
||||
resource: ExternalResource,
|
||||
callback: ProgressionCallback?,
|
||||
callback: ProgressionCallback? = null,
|
||||
): FileMessage =
|
||||
@Suppress("DEPRECATION", "DEPRECATION_ERROR") this.filesRoot.resolve(path).upload(resource, callback)
|
||||
|
||||
@ -633,13 +636,14 @@ public actual interface RemoteFile {
|
||||
@Deprecated(
|
||||
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
|
||||
ReplaceWith("this.files.uploadNewFile(path, resource, callback)"),
|
||||
level = DeprecationLevel.WARNING
|
||||
level = DeprecationLevel.ERROR
|
||||
) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
public actual suspend fun <C : FileSupported> C.sendFile(
|
||||
@DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
|
||||
@Suppress("_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public suspend fun <C : FileSupported> C.sendFile(
|
||||
path: String,
|
||||
resource: ExternalResource,
|
||||
callback: ProgressionCallback?,
|
||||
callback: ProgressionCallback? = null,
|
||||
): MessageReceipt<C> =
|
||||
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
|
||||
this.filesRoot.resolve(path).upload(resource, callback).sendTo(this)
|
||||
@ -653,9 +657,9 @@ public actual interface RemoteFile {
|
||||
@Deprecated(
|
||||
"Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile",
|
||||
ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"),
|
||||
level = DeprecationLevel.WARNING
|
||||
level = DeprecationLevel.ERROR
|
||||
) // deprecated since 2.8.0-RC
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
@DeprecatedSinceMirai(warningSince = "2.8", errorSince = "2.12")
|
||||
public suspend fun <C : FileSupported> C.sendFile(
|
||||
path: String,
|
||||
file: File,
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
110
mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt
Normal file
110
mirai-core-api/src/nativeMain/kotlin/message/data/FileMessage.kt
Normal 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
|
||||
}
|
@ -314,7 +314,7 @@ public actual open class BotConfiguration { // open for Java
|
||||
@ConfigurationDsl
|
||||
public actual fun loadDeviceInfoJson(json: String) {
|
||||
deviceInfo = {
|
||||
Companion.json.decodeFromString(DeviceInfo.serializer(), json)
|
||||
DeviceInfoManager.deserialize(json, Companion.json)
|
||||
}
|
||||
}
|
||||
|
||||
@ -329,7 +329,10 @@ public actual open class BotConfiguration { // open for Java
|
||||
public actual fun fileBasedDeviceInfo(filepath: String) {
|
||||
deviceInfo = {
|
||||
val file = MiraiFile.create(workingDir).resolve(filepath)
|
||||
Json.decodeFromString(DeviceInfo.serializer(), file.readText())
|
||||
if (!file.exists()) {
|
||||
file.writeText(DeviceInfoManager.serialize(DeviceInfo.random(), json))
|
||||
}
|
||||
DeviceInfoManager.deserialize(file.readText(), json)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ public actual abstract class LoginSolver actual constructor() {
|
||||
* @return `SwingSolver` 或 `StandardCharImageLoginSolver` 或 `null`
|
||||
*/
|
||||
public actual val Default: LoginSolver?
|
||||
get() = TODO("Not yet implemented")
|
||||
get() = null
|
||||
|
||||
@Deprecated("Binary compatibility", level = DeprecationLevel.HIDDEN)
|
||||
@Suppress("unused")
|
||||
|
@ -57,11 +57,11 @@ public actual interface MiraiLogger {
|
||||
public actual companion object INSTANCE : Factory by loadService(Factory::class, fallbackImplementation = {
|
||||
object : Factory {
|
||||
override fun create(requester: KClass<*>): MiraiLogger {
|
||||
return PlatformLogger(requester.qualifiedName ?: requester.simpleName)
|
||||
return PlatformLogger(requester.simpleName ?: requester.qualifiedName)
|
||||
}
|
||||
|
||||
override fun create(requester: KClass<*>, identity: String?): MiraiLogger {
|
||||
return PlatformLogger(identity)
|
||||
return PlatformLogger(identity ?: requester.simpleName ?: requester.qualifiedName)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -76,7 +76,7 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
val mingwMain by getting {
|
||||
val mingwX64Main by getting {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,11 @@ import io.ktor.utils.io.pool.*
|
||||
/**
|
||||
* 缓存 [ByteArray] 实例的 [ObjectPool]
|
||||
*/
|
||||
public object ByteArrayPool : DefaultPool<ByteArray>(256) {
|
||||
public object ByteArrayPool : DefaultPool<ByteArray>(128) {
|
||||
/**
|
||||
* 每一个 [ByteArray] 的大小
|
||||
*/
|
||||
public const val BUFFER_SIZE: Int = 8192 * 8
|
||||
public const val BUFFER_SIZE: Int = 4096
|
||||
|
||||
override fun produceInstance(): ByteArray = ByteArray(BUFFER_SIZE)
|
||||
|
||||
|
@ -121,8 +121,12 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len
|
||||
}
|
||||
}
|
||||
|
||||
public inline fun ByteArray.toReadPacket(offset: Int = 0, length: Int = this.size - offset): ByteReadPacket =
|
||||
ByteReadPacket(this, offset = offset, length = length)
|
||||
public inline fun ByteArray.toReadPacket(
|
||||
offset: Int = 0,
|
||||
length: Int = this.size - offset,
|
||||
noinline release: (ByteArray) -> Unit = {}
|
||||
): ByteReadPacket =
|
||||
ByteReadPacket(this, offset = offset, length = length, block = release)
|
||||
|
||||
public inline fun <R> ByteArray.read(t: ByteReadPacket.() -> R): R {
|
||||
contract {
|
||||
|
@ -23,7 +23,7 @@ import kotlin.jvm.JvmName
|
||||
*/
|
||||
|
||||
/**
|
||||
* 255 -> 00 FF
|
||||
* Converts a Short to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Short.toByteArray(): ByteArray = with(toInt()) {
|
||||
byteArrayOf(
|
||||
@ -33,7 +33,7 @@ public fun Short.toByteArray(): ByteArray = with(toInt()) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 255 -> 00 00 00 FF
|
||||
* Converts an Int to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Int.toByteArray(): ByteArray = byteArrayOf(
|
||||
ushr(24).toByte(),
|
||||
@ -43,7 +43,7 @@ public fun Int.toByteArray(): ByteArray = byteArrayOf(
|
||||
)
|
||||
|
||||
/**
|
||||
* 255 -> 00 00 00 FF
|
||||
* Converts a Long to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Long.toByteArray(): ByteArray = byteArrayOf(
|
||||
(ushr(56) and 0xFF).toByte(),
|
||||
@ -56,10 +56,13 @@ public fun Long.toByteArray(): ByteArray = byteArrayOf(
|
||||
(ushr(0) and 0xFF).toByte()
|
||||
)
|
||||
|
||||
/**
|
||||
* Converts an Int to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Int.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
|
||||
|
||||
/**
|
||||
* 255 -> 00 FF
|
||||
* Converts an UShort to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun UShort.toByteArray(): ByteArray = with(toUInt()) {
|
||||
byteArrayOf(
|
||||
@ -68,22 +71,33 @@ public fun UShort.toByteArray(): ByteArray = with(toUInt()) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Short to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Short.toUHexString(separator: String = " "): String = this.toUShort().toUHexString(separator)
|
||||
|
||||
/**
|
||||
* Converts an UShort to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun UShort.toUHexString(separator: String = " "): String =
|
||||
this.toInt().shr(8).toUShort().toUByte().toUHexString() + separator + this.toUByte().toUHexString()
|
||||
|
||||
/**
|
||||
* Converts an ULong to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun ULong.toUHexString(separator: String = " "): String =
|
||||
this.toLong().toUHexString(separator)
|
||||
|
||||
/**
|
||||
* Converts a Long to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun Long.toUHexString(separator: String = " "): String =
|
||||
this.ushr(32).toUInt().toUHexString(separator) + separator + this.toUInt().toUHexString(separator)
|
||||
|
||||
/**
|
||||
* 255 -> 00 FF
|
||||
*/
|
||||
public fun UByte.toByteArray(): ByteArray = byteArrayOf((this and 255u).toByte())
|
||||
|
||||
/**
|
||||
* Converts an UByte to its hex representation.
|
||||
*/
|
||||
public fun UByte.toUHexString(): String = this.toByte().toUHexString()
|
||||
|
||||
/**
|
||||
@ -97,7 +111,7 @@ public fun UInt.toByteArray(): ByteArray = byteArrayOf(
|
||||
)
|
||||
|
||||
/**
|
||||
* 转 [ByteArray] 后再转 hex
|
||||
* Converts an UInt to its hex representation in network order (big-endian).
|
||||
*/
|
||||
public fun UInt.toUHexString(separator: String = " "): String = this.toByteArray().toUHexString(separator)
|
||||
|
||||
@ -119,22 +133,23 @@ public fun UByte.fixToUHex(): String =
|
||||
if (this.toInt() in 0..15) "0${this.toString(16).uppercase()}" else this.toString(16).uppercase()
|
||||
|
||||
/**
|
||||
* 将 [this] 前 4 个 [Byte] 的 bits 合并为一个 [Int]
|
||||
*
|
||||
* 详细解释:
|
||||
* 一个 [Byte] 有 8 bits
|
||||
* 一个 [Int] 有 32 bits
|
||||
* 本函数将 4 个 [Byte] 的 bits 连接得到 [Int]
|
||||
* Converts 4 bytes to an UInt in network order (big-endian).
|
||||
*/
|
||||
public fun ByteArray.toUInt(): UInt =
|
||||
(this[0].toUInt().and(255u) shl 24) + (this[1].toUInt().and(255u) shl 16) + (this[2].toUInt()
|
||||
.and(255u) shl 8) + (this[3].toUInt().and(
|
||||
255u
|
||||
) shl 0)
|
||||
(this[0].toUInt().and(255u) shl 24)
|
||||
.plus(this[1].toUInt().and(255u) shl 16)
|
||||
.plus(this[2].toUInt().and(255u) shl 8)
|
||||
.plus(this[3].toUInt().and(255u) shl 0)
|
||||
|
||||
/**
|
||||
* Converts 2 bytes to an UShort in network order (big-endian).
|
||||
*/
|
||||
public fun ByteArray.toUShort(): UShort =
|
||||
((this[0].toUInt().and(255u) shl 8) + (this[1].toUInt().and(255u) shl 0)).toUShort()
|
||||
|
||||
/**
|
||||
* Converts 4 bytes to an Int in network order (big-endian).
|
||||
*/
|
||||
public fun ByteArray.toInt(offset: Int = 0): Int =
|
||||
this[offset + 0].toInt().and(255).shl(24)
|
||||
.plus(this[offset + 1].toInt().and(255).shl(16))
|
||||
|
@ -76,7 +76,7 @@ public fun MiraiFile.writeText(text: String) {
|
||||
}
|
||||
|
||||
public fun MiraiFile.readText(): String {
|
||||
return input().use { it.readText() }
|
||||
return input().use { it.readAllText() }
|
||||
}
|
||||
|
||||
public fun MiraiFile.readBytes(): ByteArray {
|
||||
|
@ -133,6 +133,8 @@ public fun Input._readTLVMap(
|
||||
return map
|
||||
}
|
||||
|
||||
public fun Input.readAllText(): String = Charsets.UTF_8.newDecoder().decode(this)
|
||||
|
||||
public inline fun Input.readString(length: Int, charset: Charset = Charsets.UTF_8): String =
|
||||
String(this.readBytes(length), charset = charset) // stdlib
|
||||
|
||||
|
@ -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!!)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
343
mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt
Normal file
343
mirai-core-utils/src/mingwX64Main/kotlin/MiraiFileImpl.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -11,4 +11,6 @@ package net.mamoe.mirai.utils
|
||||
|
||||
import platform.windows.GetCurrentProcessorNumber
|
||||
|
||||
public actual fun availableProcessors(): Int = GetCurrentProcessorNumber().toInt()
|
||||
|
||||
public actual fun availableProcessors(): Int =
|
||||
GetCurrentProcessorNumber().toInt().coerceAtLeast(4) // somehow it worked on my machine but not on CI
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -12,5 +12,7 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
public actual inline fun measureTimeMillis(block: () -> Unit): Long {
|
||||
return kotlin.system.measureTimeMillis(block)
|
||||
val start = currentTimeMillis() // getTimeMillis in stdlib doesn't work on some native targets.
|
||||
block()
|
||||
return currentTimeMillis() - start
|
||||
}
|
@ -10,10 +10,13 @@
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
internal actual fun hash(e: Throwable): Long {
|
||||
// Stacktrace analysis not available
|
||||
var hashCode = 1L
|
||||
for (stackTraceAddress in e.getStackTraceAddresses()) {
|
||||
val trace = e.getStackTraceAddresses()
|
||||
for (stackTraceAddress in trace) {
|
||||
hashCode = (hashCode xor stackTraceAddress).shl(1)
|
||||
}
|
||||
return hashCode
|
||||
|
||||
// Somehow stacktrace analysis is on my own Windows machine but not on GitHub Actions.
|
||||
// Hashing with a class to tentatively not filter out different types.
|
||||
return hashCode xor e::class.hashCode().toLongUnsigned()
|
||||
}
|
@ -215,5 +215,5 @@ internal class PosixInputForFile(val file: CPointer<FILE>) : AbstractInput() {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
internal fun PosixException.wrapIO(): IOException =
|
||||
public fun PosixException.wrapIO(): IOException =
|
||||
IOException("I/O operation failed due to posix error code $errno", this)
|
||||
|
@ -38,12 +38,18 @@ public object Services {
|
||||
}
|
||||
}
|
||||
|
||||
public fun implementations(baseClass: String): List<Any>? {
|
||||
public fun implementations(baseClass: String): List<Lazy<Any>>? {
|
||||
lock.withLock {
|
||||
return registered[baseClass]?.map { it.instance }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public fun print(): String {
|
||||
lock.withLock {
|
||||
return registered.entries.joinToString { "${it.key}:${it.value}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ -57,10 +63,10 @@ public actual fun <T : Any> loadService(
|
||||
clazz: KClass<out T>,
|
||||
fallbackImplementation: String?
|
||||
): T = loadServiceOrNull(clazz, fallbackImplementation)
|
||||
?: error("Could not load service '${clazz.qualifiedName ?: clazz}'")
|
||||
?: error("Could not load service '${clazz.qualifiedName ?: clazz}'. Current services: ${Services.print()}")
|
||||
|
||||
public actual fun <T : Any> loadServices(clazz: KClass<out T>): Sequence<T> =
|
||||
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence().orEmpty().castUp()
|
||||
Services.implementations(qualifiedNameOrFail(clazz))?.asSequence()?.map { it.value }.orEmpty().castUp()
|
||||
|
||||
private fun <T : Any> qualifiedNameOrFail(clazz: KClass<out T>) =
|
||||
clazz.qualifiedName ?: error("Could not find qualifiedName for $clazz")
|
@ -11,6 +11,8 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
import kotlinx.atomicfu.locks.ReentrantLock
|
||||
import kotlinx.atomicfu.locks.withLock
|
||||
import kotlinx.cinterop.*
|
||||
import platform.posix.*
|
||||
|
||||
@ -20,23 +22,28 @@ import platform.posix.*
|
||||
public actual fun currentTimeMillis(): Long {
|
||||
// Do not use getTimeMillis from stdlib, it doesn't support iosSimulatorArm64
|
||||
memScoped {
|
||||
val timeT = alloc<time_tVar>()
|
||||
time(timeT.ptr)
|
||||
return timeT.value.toLongUnsigned()
|
||||
val spec = alloc<timespec>()
|
||||
clock_gettime(CLOCK_REALTIME.convert(), spec.ptr)
|
||||
return (spec.tv_sec * 1000 + spec.tv_nsec / 1e6).toLong()
|
||||
}
|
||||
}
|
||||
|
||||
public actual fun currentTimeFormatted(format: String?): String {
|
||||
|
||||
private val timeLock = ReentrantLock()
|
||||
|
||||
@OptIn(UnsafeNumber::class)
|
||||
public actual fun currentTimeFormatted(format: String?): String = timeLock.withLock {
|
||||
memScoped {
|
||||
val timeT = alloc<time_tVar>()
|
||||
time(timeT.ptr)
|
||||
val tm = localtime(timeT.ptr)
|
||||
try {
|
||||
val bb = allocArray<ByteVar>(40)
|
||||
strftime(bb, 40, "%Y-%M-%d %H:%M:%S", tm);
|
||||
return bb.toKString()
|
||||
} finally {
|
||||
free(tm)
|
||||
}
|
||||
|
||||
// http://www.cplusplus.com/reference/clibrary/ctime/localtime/
|
||||
// tm returns a static pointer which doesn't need to free
|
||||
val tm = localtime(timeT.ptr) // localtime is not thread-safe
|
||||
|
||||
val bb = allocArray<ByteVar>(40)
|
||||
strftime(bb, 40, "%Y-%m-%d %H:%M:%S", tm);
|
||||
|
||||
bb.toKString()
|
||||
}
|
||||
}
|
@ -9,10 +9,12 @@
|
||||
|
||||
package net.mamoe.mirai.utils
|
||||
|
||||
private val properties: MutableMap<String, String> = ConcurrentHashMap()
|
||||
|
||||
internal actual fun getProperty(name: String, default: String): String? {
|
||||
TODO("Not yet implemented")
|
||||
return properties.getOrElse(name) { default }
|
||||
}
|
||||
|
||||
internal actual fun setProperty(name: String, value: String) {
|
||||
TODO("Not yet implemented")
|
||||
properties[name] = value
|
||||
}
|
@ -24,7 +24,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
@AfterTest
|
||||
fun afterTest() {
|
||||
println("Cleaning up...")
|
||||
baseTempDir.deleteRecursively()
|
||||
println("deleteRecursively:" + baseTempDir.deleteRecursively())
|
||||
}
|
||||
|
||||
@BeforeTest
|
||||
@ -35,8 +35,8 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
|
||||
@Test
|
||||
fun `canonical paths for canonical input`() {
|
||||
assertEquals(tempPath, tempDir.path)
|
||||
assertEquals(tempPath, tempDir.absolutePath)
|
||||
assertPathEquals(tempPath, tempDir.path)
|
||||
assertPathEquals(tempPath, tempDir.absolutePath)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -46,31 +46,26 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canonical paths for non-canonical input`() {
|
||||
open fun `canonical paths for non-canonical input`() {
|
||||
// extra /
|
||||
MiraiFile.create("$tempPath/").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
assertPathEquals("${tempPath}/test", it.path)
|
||||
assertPathEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra //
|
||||
MiraiFile.create("$tempPath//").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
assertPathEquals("${tempPath}/test", it.path)
|
||||
assertPathEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra /.
|
||||
MiraiFile.create("$tempPath/.").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
assertPathEquals("${tempPath}/test", it.path)
|
||||
assertPathEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra /./.
|
||||
MiraiFile.create("$tempPath/./.").resolve("test").let {
|
||||
assertEquals("${tempPath}/test", it.path)
|
||||
assertEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
// extra /sss/..
|
||||
MiraiFile.create("$tempPath/sss/..").resolve("test").let {
|
||||
assertEquals("${tempPath}/sss/../test", it.path) // because file is not found
|
||||
assertEquals("${tempPath}/sss/../test", it.absolutePath)
|
||||
assertPathEquals("${tempPath}/test", it.path)
|
||||
assertPathEquals("${tempPath}/test", it.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +85,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
assertFalse { tempDir.resolve("not_existing_dir").exists() }
|
||||
assertEquals(0L, tempDir.resolve("not_existing_dir").length)
|
||||
assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
|
||||
assertNotEquals(0L, tempDir.resolve("not_existing_dir").length)
|
||||
// assertNotEquals(0L, tempDir.resolve("not_existing_dir").length) // length is platform-dependent, on Windows it is 0 but on unix it is not
|
||||
assertTrue { tempDir.resolve("not_existing_dir").exists() }
|
||||
}
|
||||
|
||||
@ -98,20 +93,27 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
fun `isFile isDirectory`() {
|
||||
assertTrue { tempDir.exists() }
|
||||
|
||||
println("1")
|
||||
assertFalse { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_file.txt").isFile)
|
||||
println("1")
|
||||
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
|
||||
println("1")
|
||||
assertTrue { tempDir.resolve("not_existing_file.txt").createNewFile() }
|
||||
assertEquals(true, tempDir.resolve("not_existing_file.txt").isFile)
|
||||
assertEquals(false, tempDir.resolve("not_existing_file.txt").isDirectory)
|
||||
println("1")
|
||||
assertTrue { tempDir.resolve("not_existing_file.txt").exists() }
|
||||
|
||||
println("1")
|
||||
assertFalse { tempDir.resolve("not_existing_dir").exists() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isDirectory)
|
||||
println("1")
|
||||
assertTrue { tempDir.resolve("not_existing_dir").mkdir() }
|
||||
assertEquals(false, tempDir.resolve("not_existing_dir").isFile)
|
||||
assertEquals(true, tempDir.resolve("not_existing_dir").isDirectory)
|
||||
println("1")
|
||||
assertTrue { tempDir.resolve("not_existing_dir").exists() }
|
||||
}
|
||||
|
||||
@ -134,7 +136,7 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
|
||||
@Test
|
||||
fun readText() {
|
||||
tempDir.resolve("readText1.txt").let { file ->
|
||||
tempDir.resolve("readText2.txt").let { file ->
|
||||
assertTrue { !file.exists() }
|
||||
assertFailsWith<IOException> { file.readText() }
|
||||
|
||||
@ -143,4 +145,39 @@ internal abstract class AbstractNativeMiraiFileImplTest {
|
||||
assertEquals(text, file.readText())
|
||||
}
|
||||
}
|
||||
|
||||
private val bigText = "some text".repeat(10000)
|
||||
|
||||
@Test
|
||||
fun writeBigText() {
|
||||
// new file
|
||||
tempDir.resolve("writeText3.txt").let { file ->
|
||||
file.writeText(bigText)
|
||||
assertEquals(bigText.length, file.length.toInt())
|
||||
}
|
||||
|
||||
// override
|
||||
tempDir.resolve("writeText4.txt").let { file ->
|
||||
file.writeText(bigText)
|
||||
assertEquals(bigText.length, file.length.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readBigText() {
|
||||
tempDir.resolve("readText4.txt").let { file ->
|
||||
assertTrue { !file.exists() }
|
||||
assertFailsWith<IOException> { file.readText() }
|
||||
|
||||
file.writeText(bigText)
|
||||
println("reading text")
|
||||
val read = file.readText()
|
||||
assertEquals(bigText.length, read.length)
|
||||
assertEquals(bigText, read)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun assertPathEquals(expected: String, actual: String, message: String? = null) {
|
||||
asserter.assertEquals(message, expected.replace("\\", "/"), actual.replace("\\", "/"))
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ internal class TimeUtilsTest {
|
||||
@Test
|
||||
fun `can get currentTimeMillis`() {
|
||||
val time = currentTimeMillis()
|
||||
assertTrue(time.toString()) { time > 1642549113 }
|
||||
assertTrue(time.toString()) { time > 1654209523269 }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -157,7 +157,7 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
override fun input(): Input {
|
||||
val handle = fopen(absolutePath, "r")
|
||||
val handle = fopen(absolutePath, "rb")
|
||||
?: throw IOException(
|
||||
"Failed to open file '$absolutePath'",
|
||||
PosixException.forErrno(posixFunctionName = "fopen()")
|
||||
@ -167,7 +167,7 @@ internal actual class MiraiFileImpl actual constructor(
|
||||
|
||||
@OptIn(ExperimentalIoApi::class)
|
||||
override fun output(): Output {
|
||||
val handle = fopen(absolutePath, "w")
|
||||
val handle = fopen(absolutePath, "wb")
|
||||
?: throw IOException(
|
||||
"Failed to open file '$absolutePath'",
|
||||
PosixException.forErrno(posixFunctionName = "fopen()")
|
||||
|
@ -28,6 +28,16 @@ internal class UnixMiraiFileImplTest : AbstractNativeMiraiFileImplTest() {
|
||||
super.parent()
|
||||
}
|
||||
|
||||
override fun `canonical paths for non-canonical input`() {
|
||||
super.`canonical paths for non-canonical input`()
|
||||
|
||||
// extra /sss/..
|
||||
MiraiFile.create("$tempPath/sss/..").resolve("test").let {
|
||||
assertPathEquals("${tempPath}/sss/../test", it.path) // because file is not found
|
||||
assertPathEquals("${tempPath}/sss/../test", it.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun `resolve absolute`() {
|
||||
MiraiFile.create("$tempPath/").resolve("/Users").let {
|
||||
|
3
mirai-core/.gitignore
vendored
3
mirai-core/.gitignore
vendored
@ -1 +1,2 @@
|
||||
src/jvmTest/kotlin/local
|
||||
src/jvmTest/kotlin/local
|
||||
test-sandbox/
|
@ -10,6 +10,7 @@
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
import BinaryCompatibilityConfigurator.configureBinaryValidators
|
||||
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
@ -57,6 +58,8 @@ kotlin {
|
||||
implementation(bouncycastle)
|
||||
implementation(`log4j-api`)
|
||||
implementation(`netty-all`)
|
||||
implementation(`ktor-client-okhttp`)
|
||||
api(`kotlinx-coroutines-core`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,6 +105,55 @@ kotlin {
|
||||
dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
NATIVE_TARGETS.forEach { target ->
|
||||
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("OpenSSL")
|
||||
.apply {
|
||||
defFile = projectDir.resolve("src/nativeMain/cinterop/OpenSSL.def")
|
||||
packageName("openssl")
|
||||
}
|
||||
}
|
||||
|
||||
UNIX_LIKE_TARGETS.forEach { target ->
|
||||
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket")
|
||||
.apply {
|
||||
defFile = projectDir.resolve("src/unixMain/cinterop/Socket.def")
|
||||
packageName("sockets")
|
||||
}
|
||||
}
|
||||
|
||||
WIN_TARGETS.forEach { target ->
|
||||
(targets.getByName(target) as KotlinNativeTarget).compilations.getByName("main").cinterops.create("Socket")
|
||||
.apply {
|
||||
defFile = projectDir.resolve("src/mingwX64Main/cinterop/Socket.def")
|
||||
packageName("sockets")
|
||||
}
|
||||
}
|
||||
|
||||
configure(WIN_TARGETS.map { getByName(it + "Main") }) {
|
||||
dependencies {
|
||||
implementation(`ktor-client-curl`)
|
||||
}
|
||||
}
|
||||
|
||||
configure(LINUX_TARGETS.map { getByName(it + "Main") }) {
|
||||
dependencies {
|
||||
implementation(`ktor-client-curl`)
|
||||
}
|
||||
}
|
||||
|
||||
val darwinMain by getting {
|
||||
dependencies {
|
||||
implementation(`ktor-client-ios`)
|
||||
}
|
||||
}
|
||||
|
||||
disableCrossCompile()
|
||||
// val unixMain by getting {
|
||||
// dependencies {
|
||||
// implementation(`ktor-client-cio`)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
127
mirai-core/src/androidTest/kotlin/android/util/Log.kt
Normal file
127
mirai-core/src/androidTest/kotlin/android/util/Log.kt
Normal 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
|
||||
}
|
@ -7,9 +7,12 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmName("MiraiImplKt_common")
|
||||
|
||||
package net.mamoe.mirai.internal
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
@ -31,25 +34,14 @@ import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
|
||||
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl.Companion.impl
|
||||
import net.mamoe.mirai.internal.event.EventChannelToEventDispatcherAdapter
|
||||
import net.mamoe.mirai.internal.event.InternalEventMechanism
|
||||
import net.mamoe.mirai.internal.message.*
|
||||
import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
|
||||
import net.mamoe.mirai.internal.message.EmptyRefineContext
|
||||
import net.mamoe.mirai.internal.message.RefineContext
|
||||
import net.mamoe.mirai.internal.message.SimpleRefineContext
|
||||
import net.mamoe.mirai.internal.message.data.*
|
||||
import net.mamoe.mirai.internal.message.data.FileMessageImpl
|
||||
import net.mamoe.mirai.internal.message.data.OfflineAudioImpl
|
||||
import net.mamoe.mirai.internal.message.data.OnlineAudioImpl
|
||||
import net.mamoe.mirai.internal.message.data.UnsupportedMessageImpl
|
||||
import net.mamoe.mirai.internal.message.image.*
|
||||
import net.mamoe.mirai.internal.message.image.OfflineGroupImage
|
||||
import net.mamoe.mirai.internal.message.image.OnlineFriendImageImpl
|
||||
import net.mamoe.mirai.internal.message.image.OnlineGroupImageImpl
|
||||
import net.mamoe.mirai.internal.message.source.*
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromFriendImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromGroupImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromStrangerImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceFromTempImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToFriendImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToStrangerImpl
|
||||
import net.mamoe.mirai.internal.message.source.OnlineMessageSourceToTempImpl
|
||||
import net.mamoe.mirai.internal.message.toMessageChainNoSource
|
||||
import net.mamoe.mirai.internal.network.components.EventDispatcher
|
||||
import net.mamoe.mirai.internal.network.highway.ChannelKind
|
||||
import net.mamoe.mirai.internal.network.highway.ResourceKind
|
||||
@ -77,14 +69,21 @@ import net.mamoe.mirai.message.MessageSerializers
|
||||
import net.mamoe.mirai.message.action.Nudge
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.*
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
internal fun getMiraiImpl() = Mirai as MiraiImpl
|
||||
|
||||
internal expect fun createDefaultHttpClient(): HttpClient
|
||||
|
||||
@Suppress("FunctionName")
|
||||
internal expect fun _MiraiImpl_static_init()
|
||||
|
||||
@OptIn(LowLevelApi::class)
|
||||
// not object for ServiceLoader.
|
||||
internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
companion object {
|
||||
init {
|
||||
_MiraiImpl_static_init()
|
||||
MessageSerializers.registerSerializer(OfflineGroupImage::class, OfflineGroupImage.serializer())
|
||||
MessageSerializers.registerSerializer(OfflineFriendImage::class, OfflineFriendImage.serializer())
|
||||
MessageSerializers.registerSerializer(OnlineFriendImageImpl::class, OnlineFriendImageImpl.serializer())
|
||||
@ -156,13 +155,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
override var FileCacheStrategy: FileCacheStrategy = net.mamoe.mirai.utils.FileCacheStrategy.PlatformDefault
|
||||
|
||||
@Deprecated("Mirai is not going to use ktor. This is deprecated for removal.", level = DeprecationLevel.WARNING)
|
||||
override var Http: HttpClient = HttpClient() {
|
||||
install(HttpTimeout) {
|
||||
this.requestTimeoutMillis = 30_0000
|
||||
this.connectTimeoutMillis = 30_0000
|
||||
this.socketTimeoutMillis = 30_0000
|
||||
}
|
||||
}
|
||||
override var Http: HttpClient = createDefaultHttpClient()
|
||||
|
||||
override suspend fun acceptNewFriendRequest(event: NewFriendRequestEvent) {
|
||||
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
|
||||
@ -870,17 +863,21 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
|
||||
return main.toForwardMessageNodes(bot, context)
|
||||
}
|
||||
|
||||
protected open suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node {
|
||||
private suspend fun MsgComm.Msg.toNode(bot: Bot, refineContext: RefineContext): ForwardMessage.Node {
|
||||
val msg = this
|
||||
|
||||
@Suppress("USELESS_CAST") // compiler bug, do not remove
|
||||
val senderName = (msg.msgHead.groupInfo?.groupCard
|
||||
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
|
||||
?: msg.msgHead.fromUin.toString()) as String
|
||||
val chain = listOf(msg)
|
||||
.toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
|
||||
.refineDeep(bot, refineContext)
|
||||
return ForwardMessage.Node(
|
||||
senderId = msg.msgHead.fromUin,
|
||||
time = msg.msgHead.msgTime,
|
||||
senderName = msg.msgHead.groupInfo?.groupCard
|
||||
?: msg.msgHead.fromNick.takeIf { it.isNotEmpty() }
|
||||
?: msg.msgHead.fromUin.toString(),
|
||||
messageChain = listOf(msg)
|
||||
.toMessageChainNoSource(bot, 0, MessageSourceKind.GROUP)
|
||||
.refineDeep(bot, refineContext)
|
||||
senderName = senderName,
|
||||
messageChain = chain
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,6 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
|
||||
import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
|
||||
import net.mamoe.mirai.internal.utils.ImagePatcher
|
||||
import net.mamoe.mirai.internal.utils.RemoteFileImpl
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.internal.utils.subLogger
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
@ -117,8 +116,18 @@ private val logger by lazy {
|
||||
internal fun Bot.nickIn(context: Contact): String =
|
||||
if (context is Group) context.botAsMember.nameCardOrNick else bot.nick
|
||||
|
||||
internal expect class GroupImpl constructor(
|
||||
bot: QQAndroidBot,
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
id: Long,
|
||||
groupInfo: GroupInfo,
|
||||
members: ContactList<NormalMemberImpl>,
|
||||
) : Group, CommonGroupImpl {
|
||||
companion object
|
||||
}
|
||||
|
||||
@Suppress("PropertyName")
|
||||
internal class GroupImpl constructor(
|
||||
internal abstract class CommonGroupImpl constructor(
|
||||
bot: QQAndroidBot,
|
||||
parentCoroutineContext: CoroutineContext,
|
||||
override val id: Long,
|
||||
@ -128,31 +137,27 @@ internal class GroupImpl constructor(
|
||||
companion object
|
||||
|
||||
val uin: Long = groupInfo.uin
|
||||
override val settings: GroupSettingsImpl = GroupSettingsImpl(this, groupInfo)
|
||||
override var name: String by settings::name
|
||||
final override val settings: GroupSettingsImpl = GroupSettingsImpl(this.cast(), groupInfo)
|
||||
final override var name: String by settings::name
|
||||
|
||||
override lateinit var owner: NormalMemberImpl
|
||||
override lateinit var botAsMember: NormalMemberImpl
|
||||
final override lateinit var owner: NormalMemberImpl
|
||||
final override lateinit var botAsMember: NormalMemberImpl
|
||||
internal val botAsMemberInitialized get() = ::botAsMember.isInitialized
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root"), level = DeprecationLevel.WARNING)
|
||||
@DeprecatedSinceMirai(warningSince = "2.8")
|
||||
override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") }
|
||||
override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
|
||||
final override val files: RemoteFiles by lazy { RemoteFilesImpl(this) }
|
||||
|
||||
val lastTalkative = atomic<NormalMemberImpl?>(null)
|
||||
|
||||
override val announcements: Announcements by lazy {
|
||||
final override val announcements: Announcements by lazy {
|
||||
AnnouncementsImpl(
|
||||
this,
|
||||
this as GroupImpl,
|
||||
bot.network.logger.subLogger("Group $id")
|
||||
)
|
||||
}
|
||||
|
||||
val groupPkgMsgParsingCache = GroupPkgMsgParsingCache()
|
||||
|
||||
private val messageProtocolStrategy: MessageProtocolStrategy<GroupImpl> = GroupMessageProtocolStrategy(this)
|
||||
private val messageProtocolStrategy: MessageProtocolStrategy<GroupImpl> = GroupMessageProtocolStrategy(this.cast())
|
||||
|
||||
override suspend fun quit(): Boolean {
|
||||
check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
|
||||
@ -162,11 +167,11 @@ internal class GroupImpl constructor(
|
||||
}
|
||||
|
||||
val response: ProfileService.GroupMngReq.GroupMngReqResponse = bot.network.sendAndExpect(
|
||||
ProfileService.GroupMngReq(bot.client, this@GroupImpl.id), 5000, 2
|
||||
ProfileService.GroupMngReq(bot.client, this@CommonGroupImpl.id), 5000, 2
|
||||
)
|
||||
check(response.errorCode == 0) {
|
||||
"Group.quit failed: $response".also {
|
||||
bot.groups.delegate.add(this@GroupImpl)
|
||||
bot.groups.delegate.add(this@CommonGroupImpl.castUp())
|
||||
}
|
||||
}
|
||||
BotLeaveEvent.Active(this).broadcast()
|
||||
@ -186,7 +191,7 @@ internal class GroupImpl constructor(
|
||||
check(!isBotMuted) { throw BotIsBeingMutedException(this, message) }
|
||||
return sendMessageImpl(
|
||||
message,
|
||||
messageProtocolStrategy,
|
||||
messageProtocolStrategy.castUp(),
|
||||
::GroupMessagePreSendEvent,
|
||||
::GroupMessagePostSendEvent.cast()
|
||||
)
|
||||
@ -220,7 +225,8 @@ internal class GroupImpl constructor(
|
||||
|
||||
when (response) {
|
||||
is ImgStore.GroupPicUp.Response.Failed -> {
|
||||
ImageUploadEvent.Failed(this@GroupImpl, resource, response.resultCode, response.message).broadcast()
|
||||
ImageUploadEvent.Failed(this@CommonGroupImpl, resource, response.resultCode, response.message)
|
||||
.broadcast()
|
||||
if (response.message == "over file size max") throw OverFileSizeMaxException()
|
||||
error("upload group image failed with reason ${response.message}")
|
||||
}
|
||||
@ -239,7 +245,7 @@ internal class GroupImpl constructor(
|
||||
it.fileId = response.fileId.toInt()
|
||||
}
|
||||
.also { it.putIntoCache() }
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
.also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
is ImgStore.GroupPicUp.Response.RequireUpload -> {
|
||||
// val servers = response.uploadIpList.zip(response.uploadPortList)
|
||||
@ -268,7 +274,7 @@ internal class GroupImpl constructor(
|
||||
)
|
||||
}.also { it.fileId = response.fileId.toInt() }
|
||||
.also { it.putIntoCache() }
|
||||
.also { ImageUploadEvent.Succeed(this@GroupImpl, resource, it).broadcast() }
|
||||
.also { ImageUploadEvent.Succeed(this@CommonGroupImpl, resource, it).broadcast() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -350,7 +356,7 @@ internal class GroupImpl constructor(
|
||||
val result = bot.network.sendAndExpect(
|
||||
TroopEssenceMsgManager.SetEssence(
|
||||
bot.client,
|
||||
this@GroupImpl.uin,
|
||||
this@CommonGroupImpl.uin,
|
||||
source.internalIds.first(),
|
||||
source.ids.first()
|
||||
), 5000, 2
|
||||
|
@ -30,6 +30,7 @@ internal abstract class MessageProtocol(
|
||||
}
|
||||
|
||||
fun collectProcessors(processorCollector: ProcessorCollector) {
|
||||
println("collectProcessors, this=$this, class=${this::class}")
|
||||
processorCollector.collectProcessorsImpl()
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
package net.mamoe.mirai.internal.network.components
|
||||
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -22,6 +23,7 @@ import net.mamoe.mirai.internal.utils.crypto.ECDHWithPublicKey
|
||||
import net.mamoe.mirai.internal.utils.crypto.defaultInitialPublicKey
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.currentTimeSeconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
||||
/**
|
||||
@ -88,7 +90,7 @@ internal class EcdhInitialPublicKeyUpdaterImpl(
|
||||
logger.info("ECDH key is invalid, start to fetch ecdh public key from server.")
|
||||
val respStr =
|
||||
@Suppress("DEPRECATION", "DEPRECATION_ERROR")
|
||||
Mirai.Http.get<String>("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}")
|
||||
withTimeout(10.seconds) { Mirai.Http.get<String>("https://keyrotate.qq.com/rotate_key?cipher_suite_ver=305&uin=${bot.client.uin}") }
|
||||
val resp = json.decodeFromString(ServerRespPOJO.serializer(), respStr)
|
||||
resp.pubKeyMeta.let { meta ->
|
||||
val isValid = ECDH.verifyPublicKey(
|
||||
|
@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
|
||||
import net.mamoe.mirai.internal.network.component.ComponentKey
|
||||
import net.mamoe.mirai.internal.network.components.ServerList.Companion.DEFAULT_SERVER_LIST
|
||||
import net.mamoe.mirai.internal.network.handler.SocketAddress
|
||||
import net.mamoe.mirai.internal.network.handler.createSocketAddress
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import net.mamoe.mirai.utils.TestOnly
|
||||
import net.mamoe.mirai.utils.info
|
||||
@ -33,7 +34,7 @@ internal data class ServerAddress(
|
||||
return "$host:$port"
|
||||
}
|
||||
|
||||
fun toSocketAddress(): SocketAddress = SocketAddress(host, port)
|
||||
fun toSocketAddress(): SocketAddress = createSocketAddress(host, port)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,8 @@ package net.mamoe.mirai.internal.network.handler
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import net.mamoe.mirai.internal.network.components.*
|
||||
import net.mamoe.mirai.internal.network.handler.selector.NetworkException
|
||||
import net.mamoe.mirai.internal.network.handler.selector.NetworkHandlerSelector
|
||||
@ -89,14 +91,67 @@ internal abstract class CommonNetworkHandler<Conn>(
|
||||
internal inner class PacketDecodePipeline(parentContext: CoroutineContext) :
|
||||
CoroutineScope by parentContext.childScope() {
|
||||
private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
|
||||
private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] }
|
||||
|
||||
fun send(raw: RawIncomingPacket) {
|
||||
|
||||
private val queue: Channel<ByteReadPacket> = Channel<ByteReadPacket>(Channel.BUFFERED) { undelivered ->
|
||||
launch { sendQueue(undelivered) }
|
||||
}.also { channel -> coroutineContext[Job]!!.invokeOnCompletion { channel.close(it) } }
|
||||
|
||||
private suspend inline fun sendQueue(packet: ByteReadPacket) {
|
||||
queue.send(packet)
|
||||
}
|
||||
|
||||
init {
|
||||
launch {
|
||||
packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" }
|
||||
val result = packetCodec.processBody(context.bot, raw)
|
||||
if (result == null) {
|
||||
collectUnknownPacket(raw)
|
||||
} else collectReceived(result)
|
||||
while (isActive) {
|
||||
val result = queue.receiveCatching()
|
||||
packetLogger.verbose { "Decoding packet: $result" }
|
||||
result.onFailure { if (it is CancellationException) return@launch }
|
||||
|
||||
result.getOrNull()?.let { packet ->
|
||||
try {
|
||||
val decoded = decodePacket(packet)
|
||||
processBody(decoded)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) return@launch
|
||||
handleExceptionInDecoding(e)
|
||||
logger.error("Error while decoding packet '${packet}'", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodePacket(packet: ByteReadPacket): RawIncomingPacket {
|
||||
return if (packetLogger.isDebugEnabled) {
|
||||
val bytes = packet.readBytes()
|
||||
logger.verbose { "Decoding: len=${bytes.size}, value=${bytes.toUHexString()}" }
|
||||
val raw = packetCodec.decodeRaw(
|
||||
ssoProcessor.ssoSession,
|
||||
bytes.toReadPacket()
|
||||
)
|
||||
logger.verbose { "Decoded: ${raw.commandName}" }
|
||||
raw
|
||||
} else {
|
||||
packetCodec.decodeRaw(
|
||||
ssoProcessor.ssoSession,
|
||||
packet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processBody(raw: RawIncomingPacket) {
|
||||
packetLogger.debug { "Packet Handling Processor: receive packet ${raw.commandName}" }
|
||||
val result = packetCodec.processBody(context.bot, raw)
|
||||
if (result == null) {
|
||||
collectUnknownPacket(raw)
|
||||
} else collectReceived(result)
|
||||
}
|
||||
|
||||
fun send(packet: ByteReadPacket) {
|
||||
queue.trySend(packet).onFailure {
|
||||
throw it ?: throw IllegalStateException("Internal error: Failed to decode '$packet' without reason.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,8 +200,6 @@ internal abstract class CommonNetworkHandler<Conn>(
|
||||
this.setState { StateConnecting(ExceptionCollector()) }
|
||||
?.resumeConnection()
|
||||
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread.
|
||||
|
||||
println("INITIALIZED RETURN")
|
||||
}
|
||||
|
||||
override fun toString(): String = "StateInitialized"
|
||||
@ -214,9 +267,6 @@ internal abstract class CommonNetworkHandler<Conn>(
|
||||
connectResult.await() // propagates exceptions
|
||||
val connection = connection.await()
|
||||
this.setState { StateLoading(connection) }
|
||||
.also {
|
||||
println(" this.setState { StateLoading(connection) }: " + it)
|
||||
}
|
||||
?.resumeConnection()
|
||||
?: this@CommonNetworkHandler.resumeConnection() // concurrently closed by other thread.
|
||||
}
|
||||
|
@ -27,9 +27,12 @@ internal expect fun interface NetworkHandlerFactory<out H : NetworkHandler> {
|
||||
}
|
||||
}
|
||||
|
||||
internal expect abstract class SocketAddress {
|
||||
val host: String
|
||||
val port: Int
|
||||
}
|
||||
internal expect abstract class SocketAddress
|
||||
|
||||
internal expect fun SocketAddress(host: String, port: Int): SocketAddress
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
internal expect fun SocketAddress.getHost(): String
|
||||
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
internal expect fun SocketAddress.getPort(): Int
|
||||
|
||||
internal expect fun createSocketAddress(host: String, port: Int): SocketAddress
|
||||
|
@ -16,6 +16,7 @@ import net.mamoe.mirai.utils.coroutineName
|
||||
import net.mamoe.mirai.utils.debug
|
||||
import net.mamoe.mirai.utils.systemProp
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.native.concurrent.ThreadLocal
|
||||
|
||||
internal class LoggingStateObserver(
|
||||
val logger: MiraiLogger,
|
||||
@ -72,6 +73,7 @@ internal class LoggingStateObserver(
|
||||
)
|
||||
}
|
||||
|
||||
@ThreadLocal
|
||||
companion object {
|
||||
/**
|
||||
* - `on`/`true` for simple logging
|
||||
|
@ -422,5 +422,5 @@ internal fun String.toIpV4Long(): Long {
|
||||
if (isEmpty()) return 0
|
||||
val split = split('.')
|
||||
if (split.size != 4) return 0
|
||||
return split.mapToByteArray { it.toByte() }.toInt().toLongUnsigned()
|
||||
return split.mapToByteArray { it.toUByte().toByte() }.toInt().toLongUnsigned()
|
||||
}
|
||||
|
@ -9,7 +9,6 @@
|
||||
|
||||
package net.mamoe.mirai.internal.pipeline
|
||||
|
||||
import io.ktor.util.collections.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import net.mamoe.mirai.internal.message.contextualBugReportException
|
||||
import net.mamoe.mirai.internal.message.protocol.outgoing.OutgoingMessagePipelineContext
|
||||
|
49
mirai-core/src/commonMain/kotlin/utils/FileSystem.kt
Normal file
49
mirai-core/src/commonMain/kotlin/utils/FileSystem.kt
Normal 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"
|
||||
}
|
||||
}
|
@ -7,11 +7,17 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:JvmName("PlatformSocketKt_common")
|
||||
|
||||
package net.mamoe.mirai.internal.utils
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.ktor.utils.io.errors.*
|
||||
import net.mamoe.mirai.internal.network.handler.SocketAddress
|
||||
import net.mamoe.mirai.internal.network.handler.getHost
|
||||
import net.mamoe.mirai.internal.network.handler.getPort
|
||||
import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
/**
|
||||
* TCP Socket.
|
||||
@ -32,7 +38,6 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel {
|
||||
* @throws ReadPacketInternalException
|
||||
*/
|
||||
override suspend fun read(): ByteReadPacket
|
||||
suspend fun connect(serverHost: String, serverPort: Int)
|
||||
|
||||
companion object {
|
||||
suspend fun connect(
|
||||
@ -48,6 +53,10 @@ internal expect class PlatformSocket : Closeable, HighwayProtocolChannel {
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend inline fun PlatformSocket.Companion.connect(address: SocketAddress): PlatformSocket {
|
||||
return connect(address.getHost(), address.getPort())
|
||||
}
|
||||
|
||||
|
||||
internal expect class SocketException : IOException {
|
||||
constructor()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -15,9 +15,7 @@ import net.mamoe.mirai.utils.hexToBytes
|
||||
|
||||
internal expect interface ECDHPrivateKey
|
||||
|
||||
internal expect interface ECDHPublicKey {
|
||||
fun getEncoded(): ByteArray
|
||||
}
|
||||
internal expect interface ECDHPublicKey
|
||||
|
||||
internal expect class ECDHKeyPairImpl : ECDHKeyPair
|
||||
|
||||
@ -48,9 +46,6 @@ internal interface ECDHKeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 椭圆曲线密码, ECDH 加密
|
||||
*/
|
||||
internal expect class ECDH(keyPair: ECDHKeyPair) {
|
||||
val keyPair: ECDHKeyPair
|
||||
|
||||
@ -62,8 +57,11 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
|
||||
companion object {
|
||||
val isECDHAvailable: Boolean
|
||||
|
||||
|
||||
/**
|
||||
* 由完整的 publicKey ByteArray 得到 [ECDHPublicKey]
|
||||
* This API is platform dependent.
|
||||
* On JVM you need to add `signHead`,
|
||||
* but on Native you need to provide a key with initial byte value 0x04 and of 65 bytes' length.
|
||||
*/
|
||||
fun constructPublicKey(key: ByteArray): ECDHPublicKey
|
||||
|
||||
@ -78,7 +76,7 @@ internal expect class ECDH(keyPair: ECDHKeyPair) {
|
||||
fun generateKeyPair(initialPublicKey: ECDHPublicKey = defaultInitialPublicKey.key): ECDHKeyPair
|
||||
|
||||
/**
|
||||
* 由一对密匙计算 shareKey
|
||||
* 由一对密匙计算服务器需要的 shareKey
|
||||
*/
|
||||
fun calculateShareKey(privateKey: ECDHPrivateKey, publicKey: ECDHPublicKey): ByteArray
|
||||
}
|
||||
@ -119,19 +117,12 @@ internal data class ECDHWithPublicKey(private val initialPublicKey: ECDHInitialP
|
||||
@Serializable
|
||||
internal data class ECDHInitialPublicKey(val version: Int = 1, val keyStr: String, val expireTime: Long = 0) {
|
||||
@Transient
|
||||
internal val key: ECDHPublicKey = keyStr.adjustToPublicKey()
|
||||
internal val key: ECDHPublicKey = keyStr.hexToBytes().adjustToPublicKey()
|
||||
}
|
||||
|
||||
internal expect val publicKeyForVerify: ECDHPublicKey
|
||||
|
||||
internal val defaultInitialPublicKey: ECDHInitialPublicKey by lazy { ECDHInitialPublicKey(keyStr = "04EBCA94D733E399B2DB96EACDD3F69A8BB0F74224E2B44E3357812211D2E62EFBC91BB553098E25E33A799ADC7F76FEB208DA7C6522CDB0719A305180CC54A82E") }
|
||||
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
|
||||
|
||||
internal fun String.adjustToPublicKey(): ECDHPublicKey {
|
||||
return this.hexToBytes().adjustToPublicKey()
|
||||
}
|
||||
|
||||
internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
|
||||
internal expect fun ByteArray.adjustToPublicKey(): ECDHPublicKey
|
||||
|
||||
return ECDH.constructPublicKey(signHead + this)
|
||||
}
|
||||
internal val ECDH.Companion.curveName get() = "prime256v1" // p-256
|
||||
|
@ -12,14 +12,12 @@ package net.mamoe.mirai.internal.event
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.first
|
||||
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.event.broadcast
|
||||
import net.mamoe.mirai.internal.test.runBlockingUnit
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertIs
|
||||
|
||||
@JvmBlockingBridge
|
||||
internal class EventChannelFlowTest : AbstractEventTest() {
|
||||
|
||||
@Test
|
||||
|
@ -21,7 +21,6 @@ import net.mamoe.mirai.event.events.MessageEvent
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.test.*
|
||||
|
||||
internal class EventChannelTest : AbstractEventTest() {
|
||||
@ -37,7 +36,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
@Test
|
||||
fun singleFilter() {
|
||||
runBlocking {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.filterIsInstance<TE>()
|
||||
.filter {
|
||||
@ -69,7 +68,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
@Test
|
||||
fun multipleFilters() {
|
||||
runBlocking {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.filterIsInstance<TE>()
|
||||
.filter {
|
||||
@ -109,7 +108,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun multipleContexts1() {
|
||||
runBlocking {
|
||||
withContext(CoroutineName("1")) {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.context(CoroutineName("2"))
|
||||
.context(CoroutineName("3"))
|
||||
@ -132,7 +131,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun multipleContexts2() {
|
||||
runBlocking {
|
||||
withContext(CoroutineName("1")) {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.context(CoroutineName("2"))
|
||||
.context(CoroutineName("3"))
|
||||
@ -156,7 +155,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun multipleContexts3() {
|
||||
runBlocking {
|
||||
withContext(CoroutineName("1")) {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.context(CoroutineName("2"))
|
||||
.subscribeOnce<TE> {
|
||||
@ -178,7 +177,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun multipleContexts4() {
|
||||
runBlocking {
|
||||
withContext(CoroutineName("1")) {
|
||||
val received = suspendCoroutine<Int> { cont ->
|
||||
val received = suspendCancellableCoroutine { cont ->
|
||||
globalEventChannel()
|
||||
.subscribeOnce<TE> {
|
||||
assertEquals("1", currentCoroutineContext()[CoroutineName]!!.name)
|
||||
@ -250,7 +249,8 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun testExceptionInFilter() {
|
||||
assertFailsWith<ExceptionInEventChannelFilterException> {
|
||||
runBlocking {
|
||||
suspendCoroutine<Int> { cont ->
|
||||
@Suppress("RemoveExplicitTypeArguments")
|
||||
suspendCancellableCoroutine<Int> { cont ->
|
||||
globalEventChannel()
|
||||
.exceptionHandler {
|
||||
cont.resumeWithException(it)
|
||||
@ -278,7 +278,7 @@ internal class EventChannelTest : AbstractEventTest() {
|
||||
fun testExceptionInSubscribe() {
|
||||
runBlocking {
|
||||
assertFailsWith<IllegalStateException> {
|
||||
suspendCoroutine<Int> { cont ->
|
||||
suspendCancellableCoroutine<Int> { cont ->
|
||||
val handler = CoroutineExceptionHandler { _, throwable ->
|
||||
cont.resumeWithException(throwable)
|
||||
}
|
||||
|
@ -60,10 +60,11 @@ internal class EventTests : AbstractEventTest() {
|
||||
resetEventListeners()
|
||||
var listeners = 0
|
||||
val counter = atomic(0)
|
||||
val channel = scope.globalEventChannel()
|
||||
for (p in EventPriority.values()) {
|
||||
repeat(2333) {
|
||||
listeners++
|
||||
scope.globalEventChannel().subscribeAlways<ParentEvent> {
|
||||
channel.subscribeAlways<ParentEvent> {
|
||||
counter.getAndIncrement()
|
||||
}
|
||||
}
|
||||
|
@ -21,21 +21,22 @@ internal class MessageProtocolFacadeTest : AbstractTest() {
|
||||
assertEquals(
|
||||
"""
|
||||
QuoteReplyProtocol
|
||||
AudioProtocol
|
||||
CustomMessageProtocol
|
||||
FaceProtocol
|
||||
FileMessageProtocol
|
||||
FlashImageProtocol
|
||||
FaceProtocol
|
||||
ImageProtocol
|
||||
MarketFaceProtocol
|
||||
MusicShareProtocol
|
||||
PokeMessageProtocol
|
||||
IgnoredMessagesProtocol
|
||||
PttMessageProtocol
|
||||
RichMessageProtocol
|
||||
TextProtocol
|
||||
VipFaceProtocol
|
||||
ForwardMessageProtocol
|
||||
LongMessageProtocol
|
||||
IgnoredMessagesProtocol
|
||||
UnsupportedMessageProtocol
|
||||
GeneralMessageSenderProtocol
|
||||
""".trimIndent(),
|
||||
|
@ -18,6 +18,7 @@ import net.mamoe.mirai.message.data.MessageSource.Key.quote
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.QuoteReply
|
||||
import net.mamoe.mirai.message.data.messageChainOf
|
||||
import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import kotlin.test.Test
|
||||
|
||||
@ -86,6 +87,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() {
|
||||
),
|
||||
),
|
||||
),
|
||||
srcMsg = EMPTY_BYTE_ARRAY
|
||||
// mirai's OfflineMessageSource has no enough information to create 'srcMsg'
|
||||
),
|
||||
),
|
||||
@ -319,6 +321,7 @@ internal class QuoteReplyProtocolTest : AbstractMessageProtocolTest() {
|
||||
),
|
||||
),
|
||||
),
|
||||
srcMsg = EMPTY_BYTE_ARRAY
|
||||
// mirai's OfflineMessageSource has no enough information to create 'srcMsg'
|
||||
),
|
||||
),
|
||||
|
@ -14,6 +14,19 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal abstract class AbstractMutableComponentStorageTest : AbstractTest() {
|
||||
|
||||
internal data class TestComponent2(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent2>
|
||||
}
|
||||
|
||||
internal data class TestComponent3(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent3>
|
||||
}
|
||||
|
||||
protected abstract fun createStorage(): MutableComponentStorage
|
||||
|
||||
@Test
|
||||
|
@ -7,15 +7,30 @@
|
||||
* https://github.com/mamoe/mirai/blob/dev/LICENSE
|
||||
*/
|
||||
|
||||
@file:OptIn(TestOnly::class)
|
||||
|
||||
package net.mamoe.mirai.internal.network.component
|
||||
|
||||
import net.mamoe.mirai.internal.test.AbstractTest
|
||||
import net.mamoe.mirai.utils.TestOnly
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertSame
|
||||
|
||||
internal class CombinedStorageTest : AbstractTest() {
|
||||
|
||||
internal data class TestComponent2(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent2>
|
||||
}
|
||||
|
||||
internal data class TestComponent3(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent3>
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can get from main`() {
|
||||
val storage = ConcurrentComponentStorage().apply {
|
||||
|
@ -24,11 +24,8 @@ import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
|
||||
import net.mamoe.mirai.internal.network.component.setAll
|
||||
import net.mamoe.mirai.internal.network.components.*
|
||||
import net.mamoe.mirai.internal.network.framework.components.TestSsoProcessor
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandler
|
||||
import net.mamoe.mirai.internal.network.handler.*
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContextImpl
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory
|
||||
import net.mamoe.mirai.internal.network.handler.SocketAddress
|
||||
import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc
|
||||
import net.mamoe.mirai.internal.utils.subLogger
|
||||
@ -175,7 +172,7 @@ internal abstract class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abs
|
||||
|
||||
//Use overrideComponents to avoid StackOverflowError when applying components
|
||||
open fun createAddress(): SocketAddress =
|
||||
overrideComponents[ServerList].pollAny().let { SocketAddress(it.host, it.port) }
|
||||
overrideComponents[ServerList].pollAny().let { createSocketAddress(it.host, it.port) }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Assertions
|
||||
|
@ -22,6 +22,7 @@ import net.mamoe.mirai.internal.test.runBlockingUnit
|
||||
import net.mamoe.mirai.utils.TestOnly
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFails
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Test whether the selector can recover the connection after first successful login.
|
||||
@ -55,7 +56,7 @@ internal class SelectorRecoveryTest : AbstractCommonNHTestWithSelector() {
|
||||
bot.components[EventDispatcher].joinBroadcast() // Wait our async connector to complete.
|
||||
|
||||
// BotOfflineMonitor immediately launches a recovery which is UNDISPATCHED, so connection is immediately recovered.
|
||||
assertState(NetworkHandler.State.CONNECTING, NetworkHandler.State.LOADING, NetworkHandler.State.OK)
|
||||
assertTrue { bot.network.state != NetworkHandler.State.CLOSED }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -19,10 +19,7 @@ import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
import net.mamoe.mirai.event.events.GroupTempMessageEvent
|
||||
import net.mamoe.mirai.internal.network.components.NoticePipelineContext.Companion.KEY_FROM_SYNC
|
||||
import net.mamoe.mirai.internal.test.runBlockingUnit
|
||||
import net.mamoe.mirai.message.data.MessageSource
|
||||
import net.mamoe.mirai.message.data.OnlineMessageSource
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.content
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
@ -124,7 +121,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
|
||||
assertEquals(1630, time)
|
||||
assertEquals(1230001, fromId)
|
||||
assertEquals(2230203, targetId)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
|
||||
}
|
||||
assertIs<PlainText>(get(1))
|
||||
assertEquals("hello", get(1).content)
|
||||
@ -205,7 +202,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
|
||||
assertEquals(1630, time)
|
||||
assertEquals(1230001, fromId)
|
||||
assertEquals(1230003, targetId)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
|
||||
}
|
||||
assertIs<PlainText>(get(1))
|
||||
assertEquals("123", get(1).content)
|
||||
@ -298,7 +295,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
|
||||
assertEquals(1630, time)
|
||||
assertEquals(1230001, fromId)
|
||||
assertEquals(1230003, targetId)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
|
||||
}
|
||||
assertIs<PlainText>(get(1))
|
||||
assertEquals("hello", get(1).content)
|
||||
@ -392,7 +389,7 @@ internal class MessageTest : AbstractNoticeProcessorTest() {
|
||||
assertEquals(1630, time)
|
||||
assertEquals(1230001, fromId)
|
||||
assertEquals(1230003, targetId)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }, originalMessage)
|
||||
assertEquals(event.message.filterNot { it is MessageSource }.toMessageChain(), originalMessage)
|
||||
}
|
||||
assertIs<PlainText>(get(1))
|
||||
assertEquals("hello", get(1).content)
|
||||
|
70
mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt
Normal file
70
mirai-core/src/commonTest/kotlin/utils/crypto/ECDHTest.kt
Normal 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;
|
||||
*/
|
||||
}
|
24
mirai-core/src/darwinMain/kotlin/MiraiImpl.kt
Normal file
24
mirai-core/src/darwinMain/kotlin/MiraiImpl.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
10
mirai-core/src/darwinMain/kotlin/package.kt
Normal file
10
mirai-core/src/darwinMain/kotlin/package.kt
Normal 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
|
10
mirai-core/src/darwinTest/kotlin/package.kt
Normal file
10
mirai-core/src/darwinTest/kotlin/package.kt
Normal 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
|
33
mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt
Normal file
33
mirai-core/src/jvmBaseMain/kotlin/MiraiImpl.kt
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
34
mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt
Normal file
34
mirai-core/src/jvmBaseMain/kotlin/contact/GroupImpl.kt
Normal 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, "/") }
|
||||
}
|
@ -12,8 +12,14 @@ package net.mamoe.mirai.internal.network.handler
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
|
||||
internal actual typealias SocketAddress = java.net.SocketAddress
|
||||
internal actual typealias SocketAddress = java.net.InetSocketAddress
|
||||
|
||||
internal actual fun SocketAddress(host: String, port: Int): SocketAddress {
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
internal actual fun SocketAddress.getHost(): String = hostString ?: error("Failed to get host from address '$this'.")
|
||||
|
||||
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
|
||||
internal actual fun SocketAddress.getPort(): Int = this.port
|
||||
|
||||
internal actual fun createSocketAddress(host: String, port: Int): SocketAddress {
|
||||
return InetSocketAddress.createUnresolved(host, port)
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
|
||||
package net.mamoe.mirai.internal.network.impl.netty
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import io.netty.bootstrap.Bootstrap
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.channel.*
|
||||
@ -20,13 +21,11 @@ import io.netty.handler.codec.MessageToByteEncoder
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.job
|
||||
import net.mamoe.mirai.internal.network.components.PacketCodec
|
||||
import net.mamoe.mirai.internal.network.components.RawIncomingPacket
|
||||
import net.mamoe.mirai.internal.network.components.SsoProcessor
|
||||
import net.mamoe.mirai.internal.network.handler.CommonNetworkHandler
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandler.State
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
|
||||
import net.mamoe.mirai.utils.cast
|
||||
import net.mamoe.mirai.utils.debug
|
||||
import java.net.SocketAddress
|
||||
import io.netty.channel.Channel as NettyChannel
|
||||
@ -34,7 +33,7 @@ import io.netty.channel.Channel as NettyChannel
|
||||
internal open class NettyNetworkHandler(
|
||||
context: NetworkHandlerContext,
|
||||
address: SocketAddress,
|
||||
) : CommonNetworkHandler<NettyChannel>(context, address) {
|
||||
) : CommonNetworkHandler<NettyChannel>(context, address.cast()) {
|
||||
override fun toString(): String {
|
||||
return "NettyNetworkHandler(context=$context, address=$address)"
|
||||
}
|
||||
@ -52,26 +51,11 @@ internal open class NettyNetworkHandler(
|
||||
// netty conn.
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private inner class ByteBufToIncomingPacketDecoder : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
|
||||
private val packetCodec: PacketCodec by lazy { context[PacketCodec] }
|
||||
private val ssoProcessor: SsoProcessor by lazy { context[SsoProcessor] }
|
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
|
||||
kotlin.runCatching {
|
||||
ctx.fireChannelRead(msg.toReadPacket().use { packet ->
|
||||
packetCodec.decodeRaw(ssoProcessor.ssoSession, packet)
|
||||
})
|
||||
}.onFailure { error ->
|
||||
handleExceptionInDecoding(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class RawIncomingPacketCollector(
|
||||
private inner class IncomingPacketDecoder(
|
||||
private val decodePipeline: PacketDecodePipeline,
|
||||
) : SimpleChannelInboundHandler<RawIncomingPacket>(RawIncomingPacket::class.java) {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: RawIncomingPacket) {
|
||||
decodePipeline.send(msg)
|
||||
) : SimpleChannelInboundHandler<ByteBuf>(ByteBuf::class.java) {
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
|
||||
decodePipeline.send(msg.toReadPacket())
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,8 +75,7 @@ internal open class NettyNetworkHandler(
|
||||
})
|
||||
.addLast("outgoing-packet-encoder", OutgoingPacketEncoder())
|
||||
.addLast(LengthFieldBasedFrameDecoder(Int.MAX_VALUE, 0, 4, -4, 4))
|
||||
.addLast(ByteBufToIncomingPacketDecoder())
|
||||
.addLast("raw-packet-collector", RawIncomingPacketCollector(decodePipeline))
|
||||
.addLast(IncomingPacketDecoder(decodePipeline))
|
||||
}
|
||||
|
||||
protected open fun createDummyDecodePipeline() = PacketDecodePipeline(this@NettyNetworkHandler.coroutineContext)
|
||||
|
@ -11,7 +11,7 @@ package net.mamoe.mirai.internal.network.impl.netty
|
||||
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandlerContext
|
||||
import net.mamoe.mirai.internal.network.handler.NetworkHandlerFactory
|
||||
import java.net.SocketAddress
|
||||
import net.mamoe.mirai.internal.network.handler.SocketAddress
|
||||
|
||||
internal object NettyNetworkHandlerFactory : NetworkHandlerFactory<NettyNetworkHandler> {
|
||||
override fun create(context: NetworkHandlerContext, address: SocketAddress): NettyNetworkHandler {
|
||||
|
@ -86,7 +86,7 @@ internal actual class PlatformSocket : Closeable, HighwayProtocolChannel {
|
||||
}
|
||||
}
|
||||
|
||||
actual suspend fun connect(serverHost: String, serverPort: Int) {
|
||||
suspend fun connect(serverHost: String, serverPort: Int) {
|
||||
runInterruptible(Dispatchers.IO) {
|
||||
socket = Socket(serverHost, serverPort)
|
||||
readChannel = socket.getInputStream().buffered()
|
||||
|
@ -11,13 +11,580 @@
|
||||
|
||||
package net.mamoe.mirai.internal.utils
|
||||
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.contact.Group
|
||||
import net.mamoe.mirai.contact.isOperator
|
||||
import net.mamoe.mirai.internal.asQQAndroidBot
|
||||
import net.mamoe.mirai.internal.contact.groupCode
|
||||
import net.mamoe.mirai.internal.message.data.FileMessageImpl
|
||||
import net.mamoe.mirai.internal.message.flags.MiraiInternalMessageFlag
|
||||
import net.mamoe.mirai.internal.network.highway.Highway
|
||||
import net.mamoe.mirai.internal.network.highway.ResourceKind
|
||||
import net.mamoe.mirai.internal.network.protocol
|
||||
import net.mamoe.mirai.internal.network.protocol.data.proto.*
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement
|
||||
import net.mamoe.mirai.internal.network.protocol.packet.chat.toResult
|
||||
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
|
||||
import net.mamoe.mirai.message.MessageReceipt
|
||||
import net.mamoe.mirai.message.data.FileMessage
|
||||
import net.mamoe.mirai.utils.*
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.RemoteFile
|
||||
import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH
|
||||
import java.io.File
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.text.toByteArray
|
||||
|
||||
private val fs = FileSystem
|
||||
|
||||
internal class RemoteFileInfo(
|
||||
val id: String, // fileId or folderId
|
||||
val isFile: Boolean,
|
||||
val path: String,
|
||||
val name: String,
|
||||
val parentFolderId: String,
|
||||
val size: Long,
|
||||
val busId: Int, // for file only
|
||||
val creatorId: Long, //ownerUin, createUin
|
||||
val createTime: Long, // uploadTime, createTime
|
||||
val modifyTime: Long,
|
||||
val downloadTimes: Int,
|
||||
val sha: ByteArray, // for file only
|
||||
val md5: ByteArray, // for file only
|
||||
) {
|
||||
companion object {
|
||||
val root = RemoteFileInfo(
|
||||
"/", false, "/", "/", "", 0, 0, 0, 0, 0, 0, EMPTY_BYTE_ARRAY, EMPTY_BYTE_ARRAY
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RemoteFile.checkIsImpl(): CommonRemoteFileImpl {
|
||||
contract { returns() implies (this@checkIsImpl is RemoteFileImpl) }
|
||||
return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.")
|
||||
}
|
||||
|
||||
internal expect class RemoteFileImpl(
|
||||
contact: Group,
|
||||
path: String, // absolute
|
||||
) : CommonRemoteFileImpl {
|
||||
constructor(contact: Group, parent: String, name: String)
|
||||
}
|
||||
|
||||
internal abstract class CommonRemoteFileImpl(
|
||||
override val contact: Group,
|
||||
override val path: String, // absolute
|
||||
) : RemoteFile {
|
||||
|
||||
override var id: String? = null
|
||||
|
||||
override val name: String
|
||||
get() = path.substringAfterLast('/')
|
||||
|
||||
private val bot get() = contact.bot.asQQAndroidBot()
|
||||
private val client get() = bot.client
|
||||
|
||||
override val parent: CommonRemoteFileImpl?
|
||||
get() {
|
||||
if (path == ROOT_PATH) return null
|
||||
val s = path.substringBeforeLast('/')
|
||||
return RemoteFileImpl(contact, s.ifEmpty { ROOT_PATH })
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer id matching.
|
||||
*/
|
||||
private suspend fun Flow<Oidb0x6d8.GetFileListRspBody.Item>.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? {
|
||||
var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null
|
||||
|
||||
val idMatching = firstOrNull {
|
||||
if (it.name == this@CommonRemoteFileImpl.name) {
|
||||
nameMatching = it
|
||||
}
|
||||
it.id == this@CommonRemoteFileImpl.id
|
||||
}
|
||||
|
||||
return idMatching ?: nameMatching
|
||||
}
|
||||
|
||||
private suspend fun getFileFolderInfo(): RemoteFileInfo? {
|
||||
val parent = parent ?: return RemoteFileInfo.root
|
||||
val info = parent.getFilesFlow()
|
||||
.filter { it.name == this.name }
|
||||
.findMatching()
|
||||
?: return null
|
||||
return when {
|
||||
info.folderInfo != null -> info.folderInfo.run {
|
||||
RemoteFileInfo(
|
||||
id = folderId,
|
||||
isFile = false,
|
||||
path = path,
|
||||
name = folderName,
|
||||
parentFolderId = parentFolderId,
|
||||
size = 0,
|
||||
busId = 0,
|
||||
creatorId = createUin,
|
||||
createTime = createTime.toLongUnsigned(),
|
||||
modifyTime = modifyTime.toLongUnsigned(),
|
||||
downloadTimes = 0,
|
||||
sha = EMPTY_BYTE_ARRAY,
|
||||
md5 = EMPTY_BYTE_ARRAY,
|
||||
)
|
||||
}
|
||||
info.fileInfo != null -> info.fileInfo.run {
|
||||
RemoteFileInfo(
|
||||
id = fileId,
|
||||
isFile = true,
|
||||
path = path,
|
||||
name = fileName,
|
||||
parentFolderId = parentFolderId,
|
||||
size = fileSize,
|
||||
busId = busId,
|
||||
creatorId = uploaderUin,
|
||||
createTime = uploadTime.toLongUnsigned(),
|
||||
modifyTime = modifyTime.toLongUnsigned(),
|
||||
downloadTimes = downloadTimes,
|
||||
sha = sha,
|
||||
md5 = md5,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileInfo?.checkExists(thisPath: String, kind: String = "Remote path"): RemoteFileInfo {
|
||||
if (this == null) throw IllegalStateException("$kind '$thisPath' does not exist.")
|
||||
return this
|
||||
}
|
||||
|
||||
override suspend fun isFile(): Boolean = this.getFileFolderInfo().checkExists(this.path).isFile
|
||||
|
||||
// compiler bug
|
||||
override suspend fun isDirectory(): Boolean = !isFile()
|
||||
override suspend fun length(): Long = this.getFileFolderInfo().checkExists(this.path).size
|
||||
override suspend fun exists(): Boolean = this.getFileFolderInfo() != null
|
||||
override suspend fun getInfo(): RemoteFile.FileInfo? {
|
||||
return getFileFolderInfo()?.run {
|
||||
RemoteFile.FileInfo(
|
||||
name = name,
|
||||
id = id,
|
||||
path = path,
|
||||
length = size,
|
||||
downloadTimes = downloadTimes,
|
||||
uploaderId = creatorId,
|
||||
uploadTime = createTime,
|
||||
lastModifyTime = modifyTime,
|
||||
sha1 = sha,
|
||||
md5 = md5,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFilesFlow(): Flow<Oidb0x6d8.GetFileListRspBody.Item> {
|
||||
val info = getFileFolderInfo() ?: return emptyFlow()
|
||||
|
||||
return flow {
|
||||
var index = 0
|
||||
while (true) {
|
||||
val list = bot.network.sendAndExpect(
|
||||
FileManagement.GetFileList(
|
||||
client,
|
||||
groupCode = contact.id,
|
||||
folderId = info.id,
|
||||
startIndex = index
|
||||
)
|
||||
).toResult("RemoteFile.listFiles").getOrThrow()
|
||||
index += list.itemList.size
|
||||
|
||||
if (list.int32RetCode != 0) return@flow
|
||||
if (list.itemList.isEmpty()) return@flow
|
||||
|
||||
emitAll(list.itemList.asFlow())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Oidb0x6d8.GetFileListRspBody.Item.resolveToFile(): RemoteFile? {
|
||||
val item = this
|
||||
return when {
|
||||
item.fileInfo != null -> {
|
||||
resolve(item.fileInfo.fileName)
|
||||
}
|
||||
item.folderInfo != null -> {
|
||||
resolve(item.folderInfo.folderName)
|
||||
}
|
||||
else -> null
|
||||
}?.also {
|
||||
it.id = item.id
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun listFiles(): Flow<RemoteFile> {
|
||||
return getFilesFlow().mapNotNull { item ->
|
||||
item.resolveToFile()
|
||||
}
|
||||
}
|
||||
|
||||
// compiler bug
|
||||
override suspend fun listFilesCollection(): List<RemoteFile> = listFiles().toList()
|
||||
|
||||
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
|
||||
@OptIn(JavaFriendlyAPI::class)
|
||||
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
|
||||
if (!lazy) return listFiles().toList().iterator()
|
||||
|
||||
return object : Iterator<RemoteFile> {
|
||||
private val queue = ArrayDeque<Oidb0x6d8.GetFileListRspBody.Item>(1)
|
||||
|
||||
@Volatile
|
||||
private var index = 0
|
||||
private var ended = false
|
||||
|
||||
private suspend fun updateItems() {
|
||||
val list = bot.network.sendAndExpect(
|
||||
FileManagement.GetFileList(
|
||||
client,
|
||||
groupCode = contact.id,
|
||||
folderId = path,
|
||||
startIndex = index
|
||||
)
|
||||
).toResult("RemoteFile.listFiles").getOrThrow()
|
||||
if (list.int32RetCode != 0 || list.itemList.isEmpty()) {
|
||||
ended = true
|
||||
return
|
||||
}
|
||||
index += list.itemList.size
|
||||
for (item in list.itemList) {
|
||||
if (item.fileInfo != null || item.folderInfo != null) queue.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
if (queue.isEmpty() && !ended) runBlocking { updateItems() }
|
||||
return queue.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun next(): RemoteFile {
|
||||
return queue.removeFirst().resolveToFile()!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun resolve(relative: String) = RemoteFileImpl(contact, this.path, relative)
|
||||
override fun resolve(relative: RemoteFile): RemoteFileImpl {
|
||||
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
|
||||
|
||||
return resolve(relative.path).also { it.id = relative.id }
|
||||
}
|
||||
|
||||
override suspend fun resolveById(id: String, deep: Boolean): RemoteFile? {
|
||||
if (this.id == id) return this
|
||||
val dirs = mutableListOf<Oidb0x6d8.GetFileListRspBody.Item>()
|
||||
getFilesFlow().mapNotNull { item ->
|
||||
when {
|
||||
item.id == id -> item.resolveToFile()
|
||||
deep && item.folderInfo != null -> {
|
||||
dirs.add(item)
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}.firstOrNull()?.let { return it }
|
||||
for (dir in dirs) {
|
||||
dir.resolveToFile()?.resolveById(id, deep)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// compiler bug
|
||||
override suspend fun resolveById(id: String): RemoteFile? = resolveById(id, deep = true)
|
||||
|
||||
override fun resolveSibling(relative: String): RemoteFileImpl {
|
||||
val parent = this.parent
|
||||
if (parent == null) {
|
||||
if (fs.normalize(relative) == ROOT_PATH) error("Root path does not have sibling paths.")
|
||||
return RemoteFileImpl(contact, ROOT_PATH)
|
||||
}
|
||||
return RemoteFileImpl(contact, parent.path, relative)
|
||||
}
|
||||
|
||||
override fun resolveSibling(relative: RemoteFile): RemoteFileImpl {
|
||||
if (relative.checkIsImpl().contact !== this.contact) error("`relative` must be obtained from the same Group as `this`.")
|
||||
|
||||
return resolveSibling(relative.path).also { it.id = relative.id }
|
||||
}
|
||||
|
||||
private fun RemoteFileInfo.isOperable(): Boolean =
|
||||
creatorId == bot.id || contact.botPermission.isOperator()
|
||||
|
||||
private fun isBotOperator(): Boolean = contact.botPermission.isOperator()
|
||||
|
||||
override suspend fun delete(): Boolean {
|
||||
val info = getFileFolderInfo() ?: return false
|
||||
if (!info.isOperable()) return false
|
||||
return when {
|
||||
info.isFile -> {
|
||||
bot.network.sendAndExpect(
|
||||
FileManagement.DeleteFile(
|
||||
client,
|
||||
groupCode = contact.id,
|
||||
busId = info.busId,
|
||||
fileId = info.id,
|
||||
parentFolderId = info.parentFolderId,
|
||||
)
|
||||
).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0
|
||||
}
|
||||
// recursively -> {
|
||||
// this.listFiles().collect { child ->
|
||||
// child.delete()
|
||||
// }
|
||||
// this.delete()
|
||||
// }
|
||||
else -> {
|
||||
// natively 'recursive'
|
||||
bot.network.sendAndExpect(
|
||||
FileManagement.DeleteFolder(
|
||||
client, contact.id, info.id
|
||||
)
|
||||
).toResult("RemoteFile.delete").getOrThrow().int32RetCode == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun renameTo(name: String): Boolean {
|
||||
if (path == ROOT_PATH && name != ROOT_PATH) return false
|
||||
|
||||
val normalized = fs.normalize(name)
|
||||
if (normalized.contains('/')) throw IllegalArgumentException("'/' is not allowed in file or directory names. Given: '$name'.")
|
||||
|
||||
val info = getFileFolderInfo() ?: return false
|
||||
if (!info.isOperable()) return false
|
||||
return bot.network.sendAndExpect(
|
||||
if (info.isFile) {
|
||||
FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized)
|
||||
} else {
|
||||
FileManagement.RenameFolder(client, contact.id, info.id, normalized)
|
||||
}
|
||||
).toResult("RemoteFile.renameTo", checkResp = false).getOrThrow().int32RetCode == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* null means not exist
|
||||
*/
|
||||
private suspend fun getIdSmart(): String? {
|
||||
if (path == ROOT_PATH) return ROOT_PATH
|
||||
return this.id ?: this.getFileFolderInfo()?.id
|
||||
}
|
||||
|
||||
override suspend fun moveTo(target: RemoteFile): Boolean {
|
||||
if (target.checkIsImpl().contact != this.contact) {
|
||||
// TODO: 2021/3/4 cross-group file move
|
||||
|
||||
// target.mkdir()
|
||||
// val targetFolderId = target.getIdSmart() ?: return false
|
||||
// this.listFiles().mapNotNull { it.checkIsImpl().getFileFolderInfo() }.collect {
|
||||
// FileManagement.MoveFile(client, contact.id, it.busId, it.id, it.parentFolderId, targetFolderId)
|
||||
// .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow()
|
||||
//
|
||||
// // TODO: 2021/3/3 batch packets
|
||||
// }
|
||||
// this.delete() // it is now empty
|
||||
|
||||
error("Cross-group file operation is not yet supported.")
|
||||
}
|
||||
if (target.path == this.path) return true
|
||||
if (target.parent?.path == this.path) return false
|
||||
val info = getFileFolderInfo() ?: return false
|
||||
if (!info.isOperable()) return false
|
||||
return if (info.isFile) {
|
||||
val newParentId = target.parent?.checkIsImpl()?.getIdSmart() ?: return false
|
||||
bot.network.sendAndExpect(
|
||||
FileManagement.MoveFile(
|
||||
client,
|
||||
contact.id,
|
||||
info.busId,
|
||||
info.id,
|
||||
info.parentFolderId,
|
||||
newParentId
|
||||
)
|
||||
).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
|
||||
} else {
|
||||
return bot.network.sendAndExpect(FileManagement.RenameFolder(client, contact.id, info.id, target.name))
|
||||
.toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override suspend fun mkdir(): Boolean {
|
||||
if (path == ROOT_PATH) return false
|
||||
if (!isBotOperator()) return false
|
||||
|
||||
val parentFolderId: String = parent?.getIdSmart() ?: return false
|
||||
|
||||
return bot.network.sendAndExpect(FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name))
|
||||
.toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0
|
||||
}
|
||||
|
||||
private suspend fun upload0(
|
||||
resource: ExternalResource,
|
||||
callback: RemoteFile.ProgressionCallback?,
|
||||
): Oidb0x6d6.UploadFileRspBody? = resource.withAutoClose {
|
||||
val parent = parent ?: return null
|
||||
val parentInfo = parent.getFileFolderInfo() ?: return null
|
||||
val resp = bot.network.sendAndExpect(
|
||||
FileManagement.RequestUpload(
|
||||
client,
|
||||
groupCode = contact.id,
|
||||
folderId = parentInfo.id,
|
||||
resource = resource,
|
||||
filename = this.name
|
||||
)
|
||||
).toResult("RemoteFile.upload").getOrThrow()
|
||||
if (resp.boolFileExist) {
|
||||
return resp
|
||||
}
|
||||
|
||||
val ext = GroupFileUploadExt(
|
||||
u1 = 100,
|
||||
u2 = 1,
|
||||
entry = GroupFileUploadEntry(
|
||||
business = ExcitingBusiInfo(
|
||||
busId = resp.busId,
|
||||
senderUin = bot.id,
|
||||
receiverUin = contact.groupCode, // TODO: 2021/3/1 code or uin?
|
||||
groupCode = contact.groupCode,
|
||||
),
|
||||
fileEntry = ExcitingFileEntry(
|
||||
fileSize = resource.size,
|
||||
md5 = resource.md5,
|
||||
sha1 = resource.sha1,
|
||||
fileId = resp.fileId.toByteArray(),
|
||||
uploadKey = resp.checkKey,
|
||||
),
|
||||
clientInfo = ExcitingClientInfo(
|
||||
clientType = 2,
|
||||
appId = client.protocol.id.toString(),
|
||||
terminalType = 2,
|
||||
clientVer = "9e9c09dc",
|
||||
unknown = 4,
|
||||
),
|
||||
fileNameInfo = ExcitingFileNameInfo(this.name),
|
||||
host = ExcitingHostConfig(
|
||||
hosts = listOf(
|
||||
ExcitingHostInfo(
|
||||
url = ExcitingUrlInfo(
|
||||
unknown = 1,
|
||||
host = resp.uploadIpLanV4.firstOrNull()
|
||||
?: resp.uploadIpLanV6.firstOrNull()
|
||||
?: resp.uploadIp,
|
||||
),
|
||||
port = resp.uploadPort,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
u3 = 0,
|
||||
).toByteArray(GroupFileUploadExt.serializer())
|
||||
|
||||
callback?.onBegin(this, resource)
|
||||
|
||||
kotlin.runCatching {
|
||||
Highway.uploadResourceBdh(
|
||||
bot = bot,
|
||||
resource = resource,
|
||||
kind = ResourceKind.GROUP_FILE,
|
||||
commandId = 71,
|
||||
extendInfo = ext,
|
||||
dataFlag = 0,
|
||||
callback = if (callback == null) null else fun(it: Long) {
|
||||
callback.onProgression(this, resource, it)
|
||||
}
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
callback?.onSuccess(this, resource)
|
||||
},
|
||||
onFailure = {
|
||||
callback?.onFailure(this, resource, it)
|
||||
}
|
||||
)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
private suspend fun uploadInternal(
|
||||
resource: ExternalResource,
|
||||
callback: RemoteFile.ProgressionCallback?,
|
||||
): FileMessage {
|
||||
val resp = upload0(resource, callback) ?: error("Failed to upload file.")
|
||||
return FileMessageImpl(
|
||||
resp.fileId, resp.busId, name, resource.size, allowSend = true
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use uploadAndSend instead.",
|
||||
replaceWith = ReplaceWith("this.uploadAndSend(resource, callback)"),
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
override suspend fun upload(
|
||||
resource: ExternalResource,
|
||||
callback: RemoteFile.ProgressionCallback?,
|
||||
): FileMessage {
|
||||
val msg = uploadInternal(resource, callback)
|
||||
contact.sendMessage(msg + MiraiInternalMessageFlag)
|
||||
return msg
|
||||
}
|
||||
|
||||
// compiler bug
|
||||
@Deprecated(
|
||||
"Use uploadAndSend instead.",
|
||||
replaceWith = ReplaceWith("this.uploadAndSend(resource)"),
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
@Suppress("DEPRECATION_ERROR")
|
||||
override suspend fun upload(resource: ExternalResource): FileMessage {
|
||||
return upload(resource, null)
|
||||
}
|
||||
|
||||
override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt<Contact> {
|
||||
@Suppress("DEPRECATION")
|
||||
return contact.sendMessage(uploadInternal(resource, null) + MiraiInternalMessageFlag)
|
||||
}
|
||||
|
||||
override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? {
|
||||
val info = getFileFolderInfo() ?: return null
|
||||
if (!info.isFile) return null
|
||||
val resp = bot.network.sendAndExpect(
|
||||
FileManagement.RequestDownload(
|
||||
client,
|
||||
groupCode = contact.id,
|
||||
busId = info.busId,
|
||||
fileId = info.id
|
||||
)
|
||||
).toResult("RemoteFile.getDownloadInfo").getOrThrow()
|
||||
|
||||
return RemoteFile.DownloadInfo(
|
||||
filename = name,
|
||||
id = info.id,
|
||||
path = path,
|
||||
url = "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" +
|
||||
info.id.toByteArray().toUHexString(""),
|
||||
sha1 = info.sha,
|
||||
md5 = info.md5
|
||||
)
|
||||
}
|
||||
|
||||
override fun toString(): String = path
|
||||
|
||||
override suspend fun toMessage(): FileMessage? {
|
||||
val info = getFileFolderInfo() ?: return null
|
||||
if (!info.isFile) return null
|
||||
return FileMessageImpl(info.id, info.busId, name, info.size)
|
||||
}
|
||||
}
|
||||
|
||||
internal actual class RemoteFileImpl actual constructor(
|
||||
contact: Group,
|
||||
|
@ -12,6 +12,7 @@
|
||||
package net.mamoe.mirai.internal.utils.crypto
|
||||
|
||||
import net.mamoe.mirai.utils.decodeBase64
|
||||
import net.mamoe.mirai.utils.hexToBytes
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
@ -36,8 +37,13 @@ internal actual class ECDHKeyPairImpl(
|
||||
}
|
||||
|
||||
|
||||
internal actual val publicKeyForVerify: ECDHPublicKey by lazy {
|
||||
internal val publicKeyForVerify: ECDHPublicKey by lazy {
|
||||
KeyFactory.getInstance("RSA")
|
||||
.generatePublic(X509EncodedKeySpec("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuJTW4abQJXeVdAODw1CamZH4QJZChyT08ribet1Gp0wpSabIgyKFZAOxeArcCbknKyBrRY3FFI9HgY1AyItH8DOUe6ajDEb6c+vrgjgeCiOiCVyum4lI5Fmp38iHKH14xap6xGaXcBccdOZNzGT82sPDM2Oc6QYSZpfs8EO7TYT7KSB2gaHz99RQ4A/Lel1Vw0krk+DescN6TgRCaXjSGn268jD7lOO23x5JS1mavsUJtOZpXkK9GqCGSTCTbCwZhI33CpwdQ2EHLhiP5RaXZCio6lksu+d8sKTWU1eEiEb3cQ7nuZXLYH7leeYFoPtbFV4RicIWp0/YG+RP7rLPCwIDAQAB".decodeBase64()))
|
||||
}
|
||||
|
||||
private val signHead = "3059301306072a8648ce3d020106082a8648ce3d030107034200".hexToBytes()
|
||||
|
||||
internal actual fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
|
||||
return ECDH.constructPublicKey(signHead + this)
|
||||
}
|
||||
|
@ -12,19 +12,20 @@ package net.mamoe.mirai.internal.network.component
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal data class TestComponent2(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent2>
|
||||
}
|
||||
|
||||
internal data class TestComponent3(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent3>
|
||||
}
|
||||
|
||||
internal class ConcurrentComponentStorageTest : AbstractMutableComponentStorageTest() {
|
||||
internal data class TestComponent2(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent2>
|
||||
}
|
||||
|
||||
internal data class TestComponent3(
|
||||
val value: Int
|
||||
) {
|
||||
companion object : ComponentKey<TestComponent3>
|
||||
}
|
||||
|
||||
override fun createStorage(): MutableComponentStorage = ConcurrentComponentStorage(showAllComponents = true)
|
||||
|
||||
@Test
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user