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