diff --git a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api index c37336606..68528b0bd 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -328,6 +328,7 @@ public final class net/mamoe/mirai/contact/ExceptionsKt { } public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact { + public abstract fun getFiles ()Lnet/mamoe/mirai/contact/file/RemoteFiles; public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; } @@ -749,6 +750,112 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt { public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot; } +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public abstract fun getExpiryTime ()J + public abstract fun getMd5 ()[B + public abstract fun getSha1 ()[B + public abstract fun getSize ()J + public fun getUrl ()Ljava/lang/String; + public abstract fun getUrl (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;)Z + public abstract fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage; +} + +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion; + public fun delete ()Z + public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun exists ()Z + public abstract fun exists (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getAbsolutePath ()Ljava/lang/String; + public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public static fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public abstract fun getId ()Ljava/lang/String; + public abstract fun getLastModifiedTime ()J + public abstract fun getName ()Ljava/lang/String; + public static fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public abstract fun getParent ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun getUploadTime ()J + public abstract fun getUploaderId ()J + public abstract fun isFile ()Z + public abstract fun isFolder ()Z + public fun refresh ()Z + public abstract fun refresh (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renameTo (Ljava/lang/String;)Z + public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion { + public final fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public final fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFolder : net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFolder$Companion; + public static final field ROOT_FOLDER_ID Ljava/lang/String; + public fun children ()Lkotlinx/coroutines/flow/Flow; + public abstract fun children (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun childrenStream ()Ljava/util/stream/Stream; + public abstract fun childrenStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun createFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun createFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun files ()Lkotlinx/coroutines/flow/Flow; + public abstract fun files (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun filesStream ()Ljava/util/stream/Stream; + public abstract fun filesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun folders ()Lkotlinx/coroutines/flow/Flow; + public abstract fun folders (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun foldersStream ()Ljava/util/stream/Stream; + public abstract fun foldersStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getContentsCount ()I + public fun isEmpty ()Z + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveAll (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public abstract fun resolveAll (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveAllStream (Ljava/lang/String;)Ljava/util/stream/Stream; + public abstract fun resolveAllStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFileById (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun resolveFileById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFileById (Ljava/lang/String;Z)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun resolveFileById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun resolveFiles (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public abstract fun resolveFiles (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFilesStream (Ljava/lang/String;)Ljava/util/stream/Stream; + public abstract fun resolveFilesStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun resolveFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/contact/file/AbsoluteFolder$Companion { + public static final field ROOT_FOLDER_ID Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles { + public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public abstract fun getRoot ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage { public fun getBot ()Lnet/mamoe/mirai/Bot; public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact; @@ -4034,6 +4141,8 @@ public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/m public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; public abstract fun getName ()Ljava/lang/String; public abstract fun getSize ()J + public fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/utils/RemoteFile; public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -5977,6 +6086,21 @@ public final class net/mamoe/mirai/utils/OverFileSizeMaxException : java/lang/Il public fun ()V } +public abstract interface class net/mamoe/mirai/utils/ProgressionCallback { + public static final field Companion Lnet/mamoe/mirai/utils/ProgressionCallback$Companion; + public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback; + public fun onBegin (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;)V + public fun onFailure (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V + public fun onFinished (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V + public fun onProgression (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V + public fun onSuccess (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V +} + +public final class net/mamoe/mirai/utils/ProgressionCallback$Companion { + public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback; + public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/ProgressionCallback; +} + public abstract interface class net/mamoe/mirai/utils/RemoteFile { public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$Companion; public static final field ROOT_PATH Ljava/lang/String; diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index bfed8b2c8..2f4520159 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -328,6 +328,7 @@ public final class net/mamoe/mirai/contact/ExceptionsKt { } public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact { + public abstract fun getFiles ()Lnet/mamoe/mirai/contact/file/RemoteFiles; public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; } @@ -749,6 +750,112 @@ public final class net/mamoe/mirai/contact/announcement/OnlineAnnouncementKt { public static final fun getBot (Lnet/mamoe/mirai/contact/announcement/OnlineAnnouncement;)Lnet/mamoe/mirai/Bot; } +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFile : net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public abstract fun getExpiryTime ()J + public abstract fun getMd5 ()[B + public abstract fun getSha1 ()[B + public abstract fun getSize ()J + public fun getUrl ()Ljava/lang/String; + public abstract fun getUrl (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;)Z + public abstract fun moveTo (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage; +} + +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion; + public fun delete ()Z + public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun exists ()Z + public abstract fun exists (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getAbsolutePath ()Ljava/lang/String; + public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public static fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public abstract fun getId ()Ljava/lang/String; + public abstract fun getLastModifiedTime ()J + public abstract fun getName ()Ljava/lang/String; + public static fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public abstract fun getParent ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun getUploadTime ()J + public abstract fun getUploaderId ()J + public abstract fun isFile ()Z + public abstract fun isFolder ()Z + public fun refresh ()Z + public abstract fun refresh (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun renameTo (Ljava/lang/String;)Z + public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/contact/file/AbsoluteFileFolder$Companion { + public final fun getExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; + public final fun getNameWithoutExtension (Lnet/mamoe/mirai/contact/file/AbsoluteFileFolder;)Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/contact/file/AbsoluteFolder : net/mamoe/mirai/contact/file/AbsoluteFileFolder { + public static final field Companion Lnet/mamoe/mirai/contact/file/AbsoluteFolder$Companion; + public static final field ROOT_FOLDER_ID Ljava/lang/String; + public fun children ()Lkotlinx/coroutines/flow/Flow; + public abstract fun children (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun childrenStream ()Ljava/util/stream/Stream; + public abstract fun childrenStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun createFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun createFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun files ()Lkotlinx/coroutines/flow/Flow; + public abstract fun files (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun filesStream ()Ljava/util/stream/Stream; + public abstract fun filesStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun folders ()Lkotlinx/coroutines/flow/Flow; + public abstract fun folders (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun foldersStream ()Ljava/util/stream/Stream; + public abstract fun foldersStream (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getContentsCount ()I + public fun isEmpty ()Z + public fun refreshed ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun refreshed (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveAll (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public abstract fun resolveAll (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveAllStream (Ljava/lang/String;)Ljava/util/stream/Stream; + public abstract fun resolveAllStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFileById (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun resolveFileById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFileById (Ljava/lang/String;Z)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun resolveFileById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun resolveFileById$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun resolveFiles (Ljava/lang/String;)Lkotlinx/coroutines/flow/Flow; + public abstract fun resolveFiles (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFilesStream (Ljava/lang/String;)Ljava/util/stream/Stream; + public abstract fun resolveFilesStream (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveFolder (Ljava/lang/String;)Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public abstract fun resolveFolder (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/AbsoluteFolder;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/contact/file/AbsoluteFolder$Companion { + public static final field ROOT_FOLDER_ID Ljava/lang/String; +} + +public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles { + public abstract fun getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public abstract fun getRoot ()Lnet/mamoe/mirai/contact/file/AbsoluteFolder; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public fun uploadNewFile (Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;ILjava/lang/Object;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public static synthetic fun uploadNewFile$default (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; +} + public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage { public fun getBot ()Lnet/mamoe/mirai/Bot; public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact; @@ -4034,6 +4141,8 @@ public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/m public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; public abstract fun getName ()Ljava/lang/String; public abstract fun getSize ()J + public fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/contact/file/AbsoluteFile; + public abstract fun toAbsoluteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;)Lnet/mamoe/mirai/utils/RemoteFile; public fun toRemoteFile (Lnet/mamoe/mirai/contact/FileSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -5977,6 +6086,21 @@ public final class net/mamoe/mirai/utils/OverFileSizeMaxException : java/lang/Il public fun ()V } +public abstract interface class net/mamoe/mirai/utils/ProgressionCallback { + public static final field Companion Lnet/mamoe/mirai/utils/ProgressionCallback$Companion; + public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback; + public fun onBegin (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;)V + public fun onFailure (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V + public fun onFinished (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V + public fun onProgression (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V + public fun onSuccess (Ljava/lang/Object;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Object;)V +} + +public final class net/mamoe/mirai/utils/ProgressionCallback$Companion { + public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/ProgressionCallback; + public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/ProgressionCallback; +} + public abstract interface class net/mamoe/mirai/utils/RemoteFile { public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$Companion; public static final field ROOT_PATH Ljava/lang/String; diff --git a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt index 467c3953d..cefccd8dc 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt @@ -10,15 +10,18 @@ package net.mamoe.mirai.contact +import net.mamoe.mirai.contact.file.RemoteFiles import net.mamoe.mirai.utils.NotStableForInheritance import net.mamoe.mirai.utils.RemoteFile /** * 支持文件操作的 [Contact]. 目前仅 [Group]. * - * 获取文件操作相关示例: [RemoteFile] + * 获取文件操作相关示例: [RemoteFiles] * * @since 2.5 + * + * @see RemoteFiles */ @NotStableForInheritance public interface FileSupported : Contact { @@ -27,5 +30,14 @@ public interface FileSupported : Contact { * * @since 2.5 */ + @Suppress("DEPRECATION") + @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) // deprecated since 2.8.0-RC public val filesRoot: RemoteFile + + /** + * 获取远程文件列表 (管理器). + * + * @since 2.8 + */ + public val files: RemoteFiles } \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFile.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFile.kt new file mode 100644 index 000000000..fe3a9bbb5 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFile.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +@file:JvmBlockingBridge +@file:Suppress("OVERLOADS_INTERFACE") + +package net.mamoe.mirai.contact.file + +import kotlinx.io.errors.IOException +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.message.data.FileMessage +import net.mamoe.mirai.utils.NotStableForInheritance + +/** + * 绝对文件标识. 精确表示一个远程文件. 不会受同名文件或目录的影响. + * + * @since 2.8 + * @see RemoteFiles + * @see AbsoluteFolder + * @see AbsoluteFileFolder + */ +@NotStableForInheritance +public interface AbsoluteFile : AbsoluteFileFolder { + /** + * 文件到期时间戳, 单位秒. + */ + public val expiryTime: Long + + /** + * 文件大小 (占用空间), 单位 byte. + */ + public val size: Long + + /** + * 文件内容 SHA-1. + */ + public val sha1: ByteArray + + /** + * 文件内容 MD5. + */ + public val md5: ByteArray + + /** + * 移动远程文件到 [folder] 目录下. 成功时返回 `true`, 当远程文件不存在时返回 `false`. + * + * 注意该操作有可能产生同名文件或目录 (当 [folder] 中已经存在一个名称为 [name] 的文件或目录时). + * + * @throws IOException 当发生网络错误时可能抛出 + * @throws IllegalStateException 当发生已知的协议错误时抛出 + * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) + */ + public suspend fun moveTo(folder: AbsoluteFolder): Boolean + + /** + * 获得下载链接 URL 字符串. 当远程文件不存在时返回 `null`. + */ + public suspend fun getUrl(): String? + + /** + * 得到表示远程文件的可以发送的 [FileMessage]. + * + * 在 [上传文件][RemoteFiles.uploadNewFile] 时就已经发送了文件消息. [toMessage] 可供之后再次发送使用. + */ + public fun toMessage(): FileMessage + + /** + * 返回更新了文件或目录信息 ([lastModifiedTime] 等) 的, 指向相同文件的 [AbsoluteFileFolder]. + * 不会更新当前 [AbsoluteFileFolder] 对象. + * + * 当远程文件或目录不存在时返回 `null`. + * + * 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用. + */ + override suspend fun refreshed(): AbsoluteFile? +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFileFolder.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFileFolder.kt new file mode 100644 index 000000000..71d1f2991 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFileFolder.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +@file:JvmBlockingBridge +@file:Suppress("OVERLOADS_INTERFACE") + +package net.mamoe.mirai.contact.file + +import kotlinx.io.errors.IOException +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.utils.NotStableForInheritance +import java.io.File + +/** + * 绝对文件或目录标识. 精确表示一个远程文件. 不会受同名文件或目录的影响. + * + * @since 2.8 + * @see RemoteFiles + * @see AbsoluteFile + * @see AbsoluteFolder + */ +@NotStableForInheritance +public sealed interface AbsoluteFileFolder { + /** + * 该对象所属 [FileSupported] + */ + public val contact: FileSupported + + /** + * 上级 [AbsoluteFileFolder]. + * + * - 当该 [AbsoluteFileFolder] 表示一个目录中的文件时返回文件所属目录的 [AbsoluteFolder]. + * - 当该 [AbsoluteFileFolder] 表示子目录时返回父目录的 [AbsoluteFolder]. + * + * 特别地, + * - 当该 [AbsoluteFileFolder] 表示根目录下的一个文件时返回根目录的 [AbsoluteFolder]. + * - 当该 [AbsoluteFileFolder] 表示根目录时返回 `null` (表示无上级). + * + * 也就是说, 若 [AbsoluteFileFolder.parent] 为 `null`, 那么该 [AbsoluteFileFolder] 就表示根目录. + */ + public val parent: AbsoluteFolder? + + /** + * 文件或目录的 ID, 即 `fileId` 或 `folderId`. 该属性由服务器维护, 通常唯一且持久. + */ + public val id: String + + /** + * 文件名或目录名. + * + * 注意, 当远程文件或目录被 (其他人) 改名时, [name] 不会变动. + * 只有在调用 [renameTo] 和 [refresh] 时才会更新. + * + * 不会包含 `:*?"<>|/\` 任一字符. + */ + public val name: String + + /** + * 绝对路径, 如 `/foo/bar.txt`. + * + * 注意, 当远程文件或目录被 (其他人) 移动到其他位置或其父目录名称改名时, [absolutePath] 不会变动. + * 只有在调用 [renameTo] 和 [refresh] 等时才会更新. + */ + public val absolutePath: String + + /** + * 表示远程文件时返回 `true`. + */ + public val isFile: Boolean + + /** + * 表示远程目录时返回 `true`. + */ + public val isFolder: Boolean + + /** + * 远程文件或目录的创建时间, 时间戳秒. + */ + public val uploadTime: Long + + /** + * 远程文件或目录的最后修改时间戳, 单位秒. + * + * 注意, 当远程文件或目录被 (其他人) 改动时, [lastModifiedTime] 不会变动. + * 只有在调用 [renameTo] 和 [refresh] 等时才会更新. + */ + public val lastModifiedTime: Long + + /** + * 上传者 ID. + */ + public val uploaderId: Long + + + /** + * 查询该远程文件或目录是否还存在于服务器. + * + * 只会精确地按 [id] 检查, 而不会考虑同名文件或目录. 当文件或目录存在时返回 `true`. + * + * 该操作不会更新 [absolutePath] 等属性. + */ + public suspend fun exists(): Boolean + + /** + * 重命名远程文件或目录, **并且**修改当前(`this`) [AbsoluteFileFolder] 的 [name]. + * 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`. + * + * 注意该操作有可能产生同名文件或目录 (当服务器已经存在一个名称为 [newName] 的文件或目录时). + * + * @throws IOException 当发生网络错误时可能抛出 + * @throws IllegalStateException 当发生已知的协议错误时抛出 + * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) + */ + public suspend fun renameTo(newName: String): Boolean + + /** + * 删除远程文件或目录. 只会根据 [id] 精确地删除一个文件或目录, 不会删除其他同名文件或目录. + * 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`. + * + * 若目录非空, 则会删除目录中的所有文件. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时抛出异常. + * + * @throws IOException 当发生网络错误时可能抛出 + * @throws IllegalStateException 当发生已知的协议错误时抛出 + * @throws PermissionDeniedException 当无管理员权限时抛出 + */ + public suspend fun delete(): Boolean + + /** + * 更新当前 [AbsoluteFileFolder] 对象的文件或目录信息 ([lastModifiedTime], [absolutePath] 等). + * 成功时返回 `true`, 当远程文件或目录不存在时返回 `false`. + */ + public suspend fun refresh(): Boolean + + /** + * 返回更新了文件或目录信息 ([lastModifiedTime] 等) 的, 指向相同文件的 [AbsoluteFileFolder]. + * 不会更新当前 [AbsoluteFileFolder] 对象. + * + * 当远程文件或目录不存在时返回 `null`. + * + * 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用. + */ + public suspend fun refreshed(): AbsoluteFileFolder? + + public override fun toString(): String + + public companion object { + /** + * 返回去掉文件后缀的文件名. 如 `foo.txt` 返回 `foo`. + * + * 注意, 当远程文件或目录被 (其他人) 改名时, [nameWithoutExtension] 不会变动. + * 只有在调用 [renameTo] 和 [refresh] 时才会更新. + * + * 不会包含 `:*?"<>|/\` 任一字符. + * + * @see File.nameWithoutExtension + */ + @get:JvmStatic + public val AbsoluteFileFolder.nameWithoutExtension: String + get() = name.substringBeforeLast('.') + + /** + * 返回文件的后缀名. 如 `foo.txt` 返回 `txt`. + * + * 注意, 当远程文件或目录被 (其他人) 改名时, [extension] 不会变动. + * 只有在调用 [renameTo] 和 [refresh] 时才会更新. + * + * 不会包含 `:*?"<>|/\` 任一字符. + * + * @see File.extension + */ + @get:JvmStatic + public val AbsoluteFileFolder.extension: String + get() = name.substringAfterLast('.', "") + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt new file mode 100644 index 000000000..3d46ec6b1 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/AbsoluteFolder.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +@file:JvmBlockingBridge +@file:Suppress("OVERLOADS_INTERFACE") + +package net.mamoe.mirai.contact.file + +import kotlinx.coroutines.flow.Flow +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.JavaFriendlyAPI +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.ProgressionCallback +import java.util.stream.Stream + +/** + * 绝对目录标识. 精确表示一个远程目录. 不会受同名文件或目录的影响. + * + * @since 2.8 + * @see RemoteFiles + * @see AbsoluteFile + * @see AbsoluteFileFolder + */ +@NotStableForInheritance +public interface AbsoluteFolder : AbsoluteFileFolder { + /** + * 当前快照中文件数量, 当有文件更新时(上传/删除文件) 该属性不会更新. + * + * 只可能通过 [refresh] 手动刷新 + * + * 特别的, 若该目录表示根目录, [contentsCount] 返回 `0`. (无法快速获取) + */ + public val contentsCount: Int + + /** + * 当该目录为空时返回 `true`. + */ + public fun isEmpty(): Boolean = contentsCount == 0 + + /** + * 返回更新了文件或目录信息 ([lastModifiedTime] 等) 的, 指向相同文件的 [AbsoluteFileFolder]. + * 不会更新当前 [AbsoluteFileFolder] 对象. + * + * 当远程文件或目录不存在时返回 `null`. + * + * 该函数会遍历上级目录的所有文件并匹配当前文件, 因此可能会非常慢, 请不要频繁使用. + */ + override suspend fun refreshed(): AbsoluteFolder? + + /////////////////////////////////////////////////////////////////////////// + // list children + /////////////////////////////////////////////////////////////////////////// + + /** + * 获取该目录下所有子目录列表. + */ + public suspend fun folders(): Flow + + /** + * 获取该目录下所有子目录列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [folders], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [folders]. + */ + @JavaFriendlyAPI + public suspend fun foldersStream(): Stream + + + /** + * 获取该目录下所有文件列表. + */ + public suspend fun files(): Flow + + /** + * 获取该目录下所有文件列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [files], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [files]. + */ + @JavaFriendlyAPI + public suspend fun filesStream(): Stream + + + /** + * 获取该目录下所有文件和子目录列表. + */ + public suspend fun children(): Flow + + /** + * 获取该目录下所有文件和子目录列表. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [children], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [children]. + */ + @JavaFriendlyAPI + public suspend fun childrenStream(): Stream + + /////////////////////////////////////////////////////////////////////////// + // resolve and upload + /////////////////////////////////////////////////////////////////////////// + + /** + * 创建一个名称为 [name] 的子目录. 返回成功创建的或已有的子目录. 当目标目录已经存在时则直接返回该目录. + * + * @throws IllegalArgumentException 当 [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出 + * @throws PermissionDeniedException 当权限不足时抛出 + */ + public suspend fun createFolder(name: String): AbsoluteFolder + + /** + * 获取一个已存在的名称为 [name] 的子目录. 当该名称的子目录不存在时返回 `null`. + * + * @throws IllegalArgumentException 当 [name] 为空或包含非法字符 (`:*?"<>|`) 时抛出 + */ + public suspend fun resolveFolder(name: String): AbsoluteFolder? + + /** + * 精确获取 [AbsoluteFile.id] 为 [id] 的文件. 在目标文件不存在时返回 `null`. 当 [deep] 为 `true` 时还会深入子目录查找. + */ + @JvmOverloads + public suspend fun resolveFileById( + id: String, + deep: Boolean = false + ): AbsoluteFile? + + /** + * 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件. + */ + public suspend fun resolveFiles( + path: String + ): Flow + + /** + * 根据路径获取指向的所有路径为 [path] 的文件列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveFiles], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveFiles]. + */ + @JavaFriendlyAPI + public suspend fun resolveFilesStream( + path: String + ): Stream + + /** + * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. + */ + public suspend fun resolveAll( + path: String + ): Flow + + /** + * 根据路径获取指向的所有路径为 [path] 的文件和目录列表. 同时支持相对路径和绝对路径. 支持获取子目录内的文件和目录. + * + * 实现细节: 为了适合 Java 调用, 实现类似为阻塞式的 [resolveAll], 因此不建议在 Kotlin 使用. 在 Kotlin 请使用 [resolveAll]. + */ + @JavaFriendlyAPI + public suspend fun resolveAllStream( + path: String + ): Stream + + /** + * 上传一个文件到该目录, 返回上传成功的文件标识. + * + * 会在必要时尝试创建远程目录. + * + * ### [filepath] + * + * - 可以是 `foo.txt` 表示该目录下的文件 "foo.txt" + * - 也可以是 `sub/foo.txt` 表示该目录的子目录 "sub" 下的文件 "foo.txt". + * - 或是绝对路径 `/sub/foo.txt` 表示根目录的 "sub" 目录下的文件 "foo.txt" + * + * @param filepath 目标文件名 + * @param content 文件内容 + * @param callback 下载进度回调, 传递的 `progression` 是已下载字节数. + * + * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) + */ + @JvmOverloads + public suspend fun uploadNewFile( + filepath: String, + content: ExternalResource, + callback: ProgressionCallback? = null, + ): AbsoluteFile + + public companion object { + /** + * 根目录 folder ID. + * @see id + */ + public const val ROOT_FOLDER_ID: String = "/" + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/file/RemoteFiles.kt b/mirai-core-api/src/commonMain/kotlin/contact/file/RemoteFiles.kt new file mode 100644 index 000000000..67a93b33f --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/file/RemoteFiles.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +@file:JvmBlockingBridge +@file:Suppress("OVERLOADS_INTERFACE") + +package net.mamoe.mirai.contact.file + +import kotlinx.coroutines.flow.Flow +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.utils.ExternalResource +import net.mamoe.mirai.utils.NotStableForInheritance +import net.mamoe.mirai.utils.ProgressionCallback +import java.io.File +import java.util.stream.Stream + +/** + * 表示远程文件列表 (管理器). + * + * [RemoteFiles] 包含一些协议接口, + * + * # 文件和目录操作 + * + * 文件和目录的父类型是 [AbsoluteFileFolder]. + * + * - [AbsoluteFile] 表示一个文件 + * - [AbsoluteFolder] 表示一个目录 + * + * 每个文件或目录都拥有一个唯一 ID: [AbsoluteFileFolder.id]. 该 ID 由服务器提供, 在重命名或移动时不会变化. + * + * 文件名可以通过 [AbsoluteFileFolder.name] 获得, 但注意文件名和其他属性都会随重命名或移动等操作更新. + * + * 除根目录 [root] 外, 每个文件或目录都拥有父目录 [AbsoluteFileFolder.parent]. + * + * # 根目录 + * + * 除了 [RemoteFiles] 中定义的捷径外, 一切文件目录操作都以获取根目录开始. 可通过 [RemoteFiles.root] 获取表示根目录的 [AbsoluteFolder]. + * + * # 绝对路径与相对路径 + * + * mirai 文件系统的绝对路径与相对路径与 Java [File] 实现的相同. + * + * 以 `/` 起始的路径表示绝对路径, 基于根目录 [root] 处理. 其他路径均表示相对路径. + * + * 可由 [AbsoluteFileFolder.absolutePath] 获取其绝对路径. 值得注意的是, 所有文件与目录对象都表示绝对路径下的目标, 因此它们都总是精确地表示一个目标, 而不受环境影响. + * + * 除重命名外, 所有文件和目录操作都默认同时支持上述两种路径. + * + * # 操作 [AbsoluteFileFolder] + * + * ## 重命名, 移动 + * + * [AbsoluteFileFolder.renameTo], [AbsoluteFile.moveTo] 提供重命名和移动功能. 注意目录不支持移动. + * + * ## 获取目录中的子目录和文件列表 + * + * 一个目录 ([AbsoluteFolder]) 可以包含多个子文件, 根目录还可以包含多个子目录 (详见下文 '目录结构限制'). + * + * 使用 [AbsoluteFolder.children] 可以获得其内子目录和文件列表 [Flow]. [AbsoluteFolder.childrenStream] 提供适合 Java 的 [Stream] 实现. + * 使用 [AbsoluteFolder.folders] 或 [AbsoluteFolder.files] 可以特定地只获取子目录或文件列表. 这些函数也有其 `*Stream` 实现. + * + * 若要根据确定的文件或目录名称获取其 [AbsoluteFileFolder] 实例, 可使用 [AbsoluteFolder.resolveFiles] 或 [AbsoluteFolder.resolveFiles]. + * 注意 [AbsoluteFolder.resolveFiles] 返回 [Flow] (其 Stream 版返回 [Stream]), 因为服务器允许多个文件有相同名称. (详见下文 '允许重名'). + * + * 若已知文件 [AbsoluteFile.id], 可通过 [AbsoluteFolder.resolveFileById] 获得该文件. + * + * ## 上传新文件 + * 可使用 [AbsoluteFolder.uploadNewFile] 上传新文件. 也可以通过 [RemoteFiles.uploadNewFile] 直接上传而跳过获取目录的步骤 (因为目录不允许同名). + * + * ## 覆盖一个旧文件 + * 服务器不允许覆盖文件. 只能通过 [AbsoluteFile.delete] 删除文件后再上传新文件. 注意新旧文件的 [AbsoluteFile.id] 会不同. + * + * # 操作权限 + * 操作一个目录时总是需要管理员权限. 若群设置 "允许任何人上传文件", 则上传文件和操作自己上传的文件时都不需要特殊权限. 注意, 操作他人的文件时总是需要管理员权限. + * + * # 服务器限制 + * + * ## 目录结构限制 + * + * 在 mirai 2.8.0 发布时, 服务器仅允许两层目录结构. 也就是说只允许根目录存在子目录, 子目录不能包含另一个子目录. + * + * 为了考虑将来服务器可能升级, mirai 没有做实现上的限制. mirai 所有操作都支持多层目录, 但进行这样的操作时将会得到服务器错误, 方法会抛出 [IllegalStateException]. + * + * ## 允许重名 + * + * 服务器允许同名目录和文件存在. 如下同名的三个文件与一个目录是允许的, 但它们的 [AbsoluteFileFolder.id] 都互不相同: + * ``` + * foo + * |- test (目录) + * |- test (文件) + * |- test (文件) + * |- test (文件) + * ``` + * 注意, 目录不允许同名. + * + * [AbsoluteFileFolder] 依据 [AbsoluteFileFolder.id] 定位文件, 而不是通过文件名. 因此 [AbsoluteFileFolder] 总是精确地代表一个文件或目录. + * + * @since 2.8 + * @see FileSupported + */ +@NotStableForInheritance +public interface RemoteFiles { + /** + * 获取表示根目录的 [AbsoluteFolder] + */ + public val root: AbsoluteFolder + + /** + * 该对象所属 [FileSupported] + */ + public val contact: FileSupported + + /** + * 上传一个文件到指定精确路径. 返回指代该远程文件的 [AbsoluteFile]. + * + * 会在必要时尝试创建远程目录. + * + * 也可以使用 [AbsoluteFolder.uploadNewFile]. + * + * @param filepath 文件路径, **包含目标文件名**. 如 `/foo/bar.txt`. 若是相对目录则基于 [根目录][root] 处理. + * @param content 文件内容 + * @param callback 下载进度回调, 传递的 `progression` 是已下载字节数. + * + * @throws PermissionDeniedException 当无管理员权限时抛出 (若群仅允许管理员上传) + */ + @JvmOverloads + public suspend fun uploadNewFile( + filepath: String, + content: ExternalResource, + callback: ProgressionCallback? = null, + ): AbsoluteFile = root.uploadNewFile(filepath, content, callback) +} + diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt index 558b0208f..91a440d13 100644 --- a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt +++ b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt @@ -19,6 +19,7 @@ import kotlinx.serialization.Serializable import net.mamoe.kjbb.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 @@ -40,6 +41,7 @@ import net.mamoe.mirai.utils.* @Serializable(FileMessage.Serializer::class) @SerialName(FileMessage.SERIAL_NAME) @NotStableForInheritance +@JvmBlockingBridge public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { /** * 服务器需要的某种 ID. @@ -74,11 +76,20 @@ public interface FileMessage : MessageContent, ConstrainSingle, CodableMessage { /** * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. */ - @JvmBlockingBridge + @Suppress("DEPRECATION") + @Deprecated("Please use toAbsoluteFile", ReplaceWith("this.toAbsoluteFile(contact)")) // deprecated since 2.8.0-RC public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { + @Suppress("DEPRECATION") return contact.filesRoot.resolveById(id) } + /** + * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. + * + * @since 2.8 + */ + public suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? + override val key: Key get() = Key /** diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index 87fe42667..2ebd057b0 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -437,6 +437,11 @@ public interface ExternalResource : Closeable { * @see RemoteFile.path * @see RemoteFile.uploadAndSend */ + @Suppress("DEPRECATION") + @Deprecated( + "Deprecated. Please use AbsoluteFolder.uploadNewFile", + ReplaceWith("contact.files.uploadNewFile(path, this, callback)") + ) // deprecated since 2.8.0-RC @JvmStatic @JvmBlockingBridge @JvmOverloads @@ -456,6 +461,11 @@ public interface ExternalResource : Closeable { * @see RemoteFile.path * @see RemoteFile.uploadAndSend */ + @Suppress("DEPRECATION") + @Deprecated( + "Deprecated. Please use AbsoluteFolder.uploadNewFile", + ReplaceWith("contact.files.uploadNewFile(path, this, callback)") + ) // deprecated since 2.8.0-RC @JvmStatic @JvmBlockingBridge @JvmName("sendAsFile") diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ProgressionCallback.kt b/mirai-core-api/src/commonMain/kotlin/utils/ProgressionCallback.kt new file mode 100644 index 000000000..7c4e491a3 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/ProgressionCallback.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.utils + +import kotlinx.coroutines.channels.SendChannel +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.utils.ProgressionCallback.Companion.asProgressionCallback + + +/** + * 操作进度回调, 可供前端使用, 以提供进度显示. + * + * @param S subject, 操作对象, 如 [AbsoluteFile] + * @param P progression, 用于提示进度. 如当下载文件时为已下载文件大小字节数 [Long]. + * + * @see asProgressionCallback + * + * @since 2.8 + */ +public interface ProgressionCallback { + /** + * 当操作开始时调用 + */ + public fun onBegin(subject: S, resource: ExternalResource) {} + + /** + * 每当有进度更新时调用. 此方法可能会同时被多个线程调用. + */ + public fun onProgression(subject: S, resource: ExternalResource, progression: P) {} + + /** + * 当操作成功时调用. + * + * 在默认实现下只会由 [onFinished] 调用 + */ + public fun onSuccess(subject: S, resource: ExternalResource, progression: P) {} + + /** + * 当操作以异常失败时调用. + * + * 在默认实现下只会由 [onFinished] 调用 + */ + public fun onFailure(subject: S, resource: ExternalResource, exception: Throwable) {} + + /** + * 当操作完成时调用. + */ + public fun onFinished(subject: S, resource: ExternalResource, result: Result

) { + result.fold( + onSuccess = { onSuccess(subject, resource, it) }, + onFailure = { onFailure(subject, resource, it) }, + ) + } + + public companion object { + /** + * 将一个 [SendChannel] 作为 [ProgressionCallback] 使用. + * + * ## 下载文件的使用示例 + * + * 每当有进度更新, 已下载的字节数都会被[发送][SendChannel.offer]到 [SendChannel] 中. + * 进度的发送会通过 [offer][SendChannel.offer], 而不是通过 [send][SendChannel.send]. 意味着 [SendChannel] 通常要实现缓存. + * + * 若 [closeOnFinish] 为 `true`, 当下载完成 (无论是失败还是成功) 时会 [关闭][SendChannel.close] [SendChannel]. + * + * 使用示例: + * ``` + * val progress = Channel(Channel.BUFFERED) + * + * launch { + * // 每 3 秒发送一次操作进度百分比 + * progress.receiveAsFlow().sample(Duration.seconds(3)).collect { bytes -> + * group.sendMessage("File upload: ${(bytes.toDouble() / resource.size * 100).toInt() / 100}%.") // 保留 2 位小数 + * } + * } + * + * group.files.uploadNewFile("/foo.txt", resource, callback = progress.asProgressionCallback(true)) + * group.sendMessage("File uploaded successfully.") + * ``` + * + * 直接使用 [ProgressionCallback] 也可以实现示例这样的功能, [asProgressionCallback] 是为了简化操作. + */ + @JvmStatic + public fun SendChannel

.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback { + return object : ProgressionCallback { + override fun onProgression(subject: S, resource: ExternalResource, progression: P) { + trySend(progression) + } + + override fun onFinished(subject: S, resource: ExternalResource, result: Result

) { + if (closeOnFinish) this@asProgressionCallback.close(result.exceptionOrNull()) + } + } + } + } +} diff --git a/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt index 6c06a8db9..572597499 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt @@ -7,7 +7,7 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ -@file:Suppress("unused") +@file:Suppress("unused", "DEPRECATION") @file:JvmBlockingBridge package net.mamoe.mirai.utils @@ -97,6 +97,7 @@ import java.io.File * @see FileSupported * @since 2.5 */ +@Deprecated("Please use RemoteFiles and AbsoluteFileFolder form fileSupported.files") // deprecated since 2.8.0-RC @NotStableForInheritance public interface RemoteFile { /** @@ -348,6 +349,10 @@ public interface RemoteFile { * 上传进度回调, 可供前端使用, 以提供进度显示. * @see asProgressionCallback */ + @Deprecated( + "Deprecated without replacement. Please use AbsoluteFolder.uploadNewFile", + ReplaceWith("contact.files.uploadNewFile(path, this, callback)") + ) // deprecated since 2.8.0-RC public interface ProgressionCallback { /** * 当上传开始时调用 @@ -610,6 +615,11 @@ public interface RemoteFile { */ @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 public suspend fun C.sendFile( path: String, resource: ExternalResource, @@ -624,6 +634,11 @@ public interface RemoteFile { */ @JvmStatic @JvmOverloads + @Deprecated( + "Deprecated. Please use AbsoluteFolder.uploadNewFile or RemoteFiles.uploadNewFile", + ReplaceWith("file.toExternalResource().use { this.files.uploadNewFile(path, it, callback) }"), + level = DeprecationLevel.WARNING + ) // deprecated since 2.8.0-RC public suspend fun C.sendFile( path: String, file: File, diff --git a/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt b/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt index 645ca7046..15de055ca 100644 --- a/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt +++ b/mirai-core-utils/src/commonMain/kotlin/ResultExtensions.kt @@ -150,4 +150,13 @@ public inline fun Result.mapFailure( block: (Throwable) -> Throwable, ): Result = onFailure { return Result.failure(block(it)) -} \ No newline at end of file +} + +public inline fun Result.onSuccessCatching(block: () -> Unit): Result { + if (isSuccess) { + runCatching(block).onFailure { + return@onSuccessCatching Result.failure(it) + } + } + return this +} diff --git a/mirai-core/src/commonMain/kotlin/contact/ContactAware.kt b/mirai-core/src/commonMain/kotlin/contact/ContactAware.kt new file mode 100644 index 000000000..2304b11aa --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/ContactAware.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2019-2021 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.Contact +import net.mamoe.mirai.internal.asQQAndroidBot + +internal interface ContactAware { + val contact: Contact + + val bot get() = contact.bot.asQQAndroidBot() + val client get() = bot.client +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index e923d88b1..420854247 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -16,12 +16,14 @@ import net.mamoe.mirai.LowLevelApi import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.announcement.Announcements +import net.mamoe.mirai.contact.file.RemoteFiles import net.mamoe.mirai.data.GroupInfo import net.mamoe.mirai.data.MemberInfo import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* import net.mamoe.mirai.internal.QQAndroidBot import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl +import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl import net.mamoe.mirai.internal.contact.info.MemberInfoImpl import net.mamoe.mirai.internal.message.* import net.mamoe.mirai.internal.network.components.BdhSession @@ -106,7 +108,10 @@ internal class GroupImpl constructor( override lateinit var owner: NormalMemberImpl override lateinit var botAsMember: NormalMemberImpl + @Suppress("DEPRECATION") + @Deprecated("Please use files instead.", replaceWith = ReplaceWith("files.root")) override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") } + override val files: RemoteFiles by lazy { RemoteFilesImpl(this) } override val announcements: Announcements by lazy { diff --git a/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFileImpl.kt b/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFileImpl.kt new file mode 100644 index 000000000..e044ba8be --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFileImpl.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2019-2021 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.file + +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.contact.file.AbsoluteFolder +import net.mamoe.mirai.internal.message.FileMessageImpl +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.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.message.data.FileMessage +import net.mamoe.mirai.utils.toUHexString + +internal class AbsoluteFileImpl( + contact: FileSupported, + parent: AbsoluteFolder?, + id: String, + name: String, + uploadTime: Long, + lastModifiedTime: Long, + uploaderId: Long, + + override var expiryTime: Long, + override val size: Long, // when file is changed, its id will also be changed, so no need to be var + override val sha1: ByteArray, + override val md5: ByteArray, + + busId: Int, +) : AbsoluteFile, AbstractAbsoluteFileFolder( + contact, parent, id, name, uploadTime, uploaderId, lastModifiedTime, + busId +) { + override fun checkPermission(operationHint: String) { + // TODO: 30/10/2021 checkPermission: 群可以设置允许任何人上传而目前没有检测这个属性, 因此不能实现权限判定 + +// if (uploaderId == bot.id) return +// if (contact is GroupImpl && !contact.botPermission.isOperator()) throwPermissionDeniedException(operationHint) +// return + } + + override val isFile: Boolean get() = true + override val isFolder: Boolean get() = false + + override val absolutePath: String + get() { + val parent = parent + return when { + parent == null || parent.name == "/" -> "/$name" + else -> "${parent.absolutePath}/$name" + } + } + + override suspend fun exists(): Boolean { + return FileManagement.GetFileInfo( + client, + groupCode = contact.id, + busId = busId, + fileId = id + ).sendAndExpect(bot) + .toResult("AbsoluteFileImpl.exists", checkResp = false) + .getOrThrow() + .fileInfo != null + } + + + override suspend fun moveTo(folder: AbsoluteFolder): Boolean { + if (folder.contact != this.contact) { + error("Cross-group file operation is not yet supported.") + } + if (folder.absolutePath == this.parentOrRoot.absolutePath) return true + checkPermission("moveTo") + + val result = FileManagement.MoveFile(client, contact.id, busId, id, parent.idOrRoot, folder.idOrRoot) + .sendAndExpect(bot).toResult("AbsoluteFileImpl.moveTo", checkResp = false) + .getOrThrow() + + return when (result.int32RetCode) { + -36 -> throwPermissionDeniedException("moveTo") + 0 -> { + parent = folder + true + } + else -> { + false + } + } +// } else { +// return FileManagement.RenameFolder(client, contact.id, id, name).sendAndExpect(bot) +// .toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 +// } + } + + override suspend fun getUrl(): String? { + // Known error + // java.lang.IllegalStateException: Failed AbsoluteFileImpl.getUrl, result=-303, msg=param error: bus_id + // java.lang.IllegalStateException: Failed AbsoluteFileImpl.getUrl, result=-103, msg=GetFileAttrAction file not exist + + val resp = FileManagement.RequestDownload( + client, + groupCode = contact.id, + busId = busId, + fileId = id + ).sendAndExpect(bot) + .toResult("AbsoluteFileImpl.getUrl") + .getOrElse { return null } + + + return "http://${resp.downloadIp}/ftn_handler/${resp.downloadUrl.toUHexString("")}/?fname=" + + id.toByteArray().toUHexString("") + } + + override fun toMessage(): FileMessage { + return FileMessageImpl(id, busId, name, size) + } + + override suspend fun refresh(): Boolean { + val new = refreshed() ?: return false + this.parent = new.parent + this.expiryTime = new.expiryTime + this.name = new.name + this.lastModifiedTime = new.lastModifiedTime + return true + } + + override fun toString(): String = "AbsoluteFile(name=$name, absolutePath=$absolutePath, id=$id)" + + override suspend fun refreshed(): AbsoluteFile? { + val result = FileManagement.GetFileInfo(client, contact.id, id, busId) + .sendAndExpect(bot) + .toResult("AbsoluteFile.refreshed") + .getOrNull()?.fileInfo + ?: return null + + return if (result.parentFolderId == this.parentOrRoot.id) { + this.parentOrRoot.impl().createChildFile(result) + } else { + null + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as AbsoluteFileImpl + + if (expiryTime != other.expiryTime) return false + if (size != other.size) return false + if (!sha1.contentEquals(other.sha1)) return false + if (!md5.contentEquals(other.md5)) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + expiryTime.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + sha1.contentHashCode() + result = 31 * result + md5.contentHashCode() + return result + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFolderImpl.kt b/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFolderImpl.kt new file mode 100644 index 000000000..b478f2ce9 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/file/AbsoluteFolderImpl.kt @@ -0,0 +1,465 @@ +/* + * Copyright 2019-2021 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.file + +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.runBlocking +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.contact.file.AbsoluteFileFolder +import net.mamoe.mirai.contact.file.AbsoluteFolder +import net.mamoe.mirai.contact.isOperator +import net.mamoe.mirai.internal.contact.GroupImpl +import net.mamoe.mirai.internal.contact.file.RemoteFilesImpl.Companion.findFileByPath +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.components.ClockHolder.Companion.clock +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.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.internal.utils.FileSystem +import net.mamoe.mirai.internal.utils.io.serialization.toByteArray +import net.mamoe.mirai.utils.* +import java.util.stream.Stream +import kotlin.streams.asStream + +internal fun Oidb0x6d8.GetFileListRspBody.Item.resolved(parent: AbsoluteFolderImpl): AbsoluteFileFolder? { + val item = this + return when { + item.fileInfo != null -> { + parent.createChildFile(item.fileInfo) + } + item.folderInfo != null -> { + parent.createChildFolder(item.folderInfo) + } + else -> null + } +} + +internal fun AbsoluteFolderImpl.createChildFolder( + folderInfo: GroupFileCommon.FolderInfo +): AbsoluteFolderImpl = AbsoluteFolderImpl( + contact = contact, + parent = this, + id = folderInfo.folderId, + name = folderInfo.folderName, + uploadTime = folderInfo.createTime.toLongUnsigned(), + uploaderId = folderInfo.createUin, + lastModifiedTime = folderInfo.modifyTime.toLongUnsigned(), + contentsCount = folderInfo.totalFileCount +) + +internal fun AbsoluteFolderImpl.createChildFile( + info: GroupFileCommon.FileInfo +): AbsoluteFileImpl = AbsoluteFileImpl( + contact = contact, + parent = this, + id = info.fileId, + name = info.fileName, + uploadTime = info.uploadTime.toLongUnsigned(), + lastModifiedTime = info.modifyTime.toLongUnsigned(), + uploaderId = info.uploaderUin, + expiryTime = info.deadTime.toLongUnsigned(), + size = info.fileSize, + sha1 = info.sha, + md5 = info.md5, + busId = info.busId +) + +internal class AbsoluteFolderImpl( + contact: FileSupported, parent: AbsoluteFolder?, id: String, name: String, + uploadTime: Long, uploaderId: Long, lastModifiedTime: Long, + override var contentsCount: Int, +) : AbstractAbsoluteFileFolder( + contact, + parent, id, name, uploadTime, uploaderId, lastModifiedTime, 0 +), AbsoluteFolder { + override fun checkPermission(operationHint: String) { + // 目录权限不受 '允许任何人上传' 设置的影响 + if (contact is GroupImpl && !contact.botPermission.isOperator()) throwPermissionDeniedException(operationHint) + return + } + + override val isFile: Boolean get() = false + override val isFolder: Boolean get() = true + + override val absolutePath: String + get() { + val parent = parent + return when { + parent == null || this.id == "/" -> "/" + parent.parent == null || parent.id == "/" -> "/$name" + else -> "${parent.absolutePath}/$name" + } + } + + companion object { + suspend fun getItemsFlow( + client: QQAndroidClient, + contact: FileSupported, + folderId: String + ): Flow { + return flow { + var index = 0 + while (true) { + val list = FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = folderId, + startIndex = index + ).sendAndExpect(client.bot).toResult("AbsoluteFolderImpl.getFilesFlow").getOrThrow() + index += list.itemList.size + + if (list.int32RetCode != 0) return@flow + if (list.itemList.isEmpty()) return@flow + + emitAll(list.itemList.asFlow()) + } + } + } + + suspend fun uploadNewFileImpl( + folder: AbsoluteFolderImpl, + filepath: String, + content: ExternalResource, + callback: ProgressionCallback? + ): AbsoluteFile { + if (filepath.isBlank()) throw IllegalArgumentException("filename cannot be blank.") + // TODO: 12/10/2021 checkPermission for AbsoluteFolderImpl.upload + + content.withAutoClose { + val resp = FileManagement.RequestUpload( + folder.client, + groupCode = folder.contact.id, + folderId = folder.id, + resource = content, + filename = filepath + ).sendAndExpect(folder.bot).toResult("AbsoluteFolderImpl.upload").getOrThrow() + + when (resp.int32RetCode) { + -36 -> folder.throwPermissionDeniedException("uploadNewFile") + } + + val file = AbsoluteFileImpl( + contact = folder.contact, + parent = folder, + id = resp.fileId, + name = filepath, + uploadTime = folder.bot.clock.server.currentTimeSeconds(), + lastModifiedTime = folder.bot.clock.server.currentTimeSeconds(), + expiryTime = 0, + uploaderId = folder.bot.id, + size = content.size, + sha1 = content.sha1, + md5 = content.md5, + busId = resp.busId + ) + + if (resp.boolFileExist) { + // resp.boolFileExist: + // 服务器是否存在相同的内容, 只是用来判断可不可以跳过上传 + // 当为 true 时跳过上传, 但仍然需要完成 `sendMessage(FileMessage)` 才是正常逻辑 + callback?.onBegin(file, content) + val result = kotlin.runCatching { + folder.contact.sendMessage(file.toMessage()) + }.map { content.size } + callback?.onFinished(file, content, result) + return file + } + + val ext = GroupFileUploadExt( + u1 = 100, + u2 = 1, + entry = GroupFileUploadEntry( + business = ExcitingBusiInfo( + busId = resp.busId, + senderUin = folder.bot.id, + receiverUin = folder.contact.id, // TODO: 2021/3/1 code or uin? + groupCode = folder.contact.id, + ), + fileEntry = ExcitingFileEntry( + fileSize = content.size, + md5 = content.md5, + sha1 = content.sha1, + fileId = resp.fileId.toByteArray(), + uploadKey = resp.checkKey, + ), + clientInfo = ExcitingClientInfo( + clientType = 2, + appId = folder.client.protocol.id.toString(), + terminalType = 2, + clientVer = "9e9c09dc", + unknown = 4, + ), + fileNameInfo = ExcitingFileNameInfo(filepath), + 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(file, content) + + kotlin.runCatching { + Highway.uploadResourceBdh( + bot = folder.bot, + resource = content, + kind = ResourceKind.GROUP_FILE, + commandId = 71, + extendInfo = ext, + dataFlag = 0, + callback = if (callback == null) null else fun(it: Long) { + callback.onProgression(file, content, it) + } + ) + }.let { result0 -> + val result = result0.onSuccessCatching { + folder.contact.sendMessage(file.toMessage()) + } + callback?.onFinished(file, content, result.map { content.size }) + } + + return file + } + } + } + + suspend fun getItemsFlow(): Flow = Companion.getItemsFlow(client, contact, id) + + @JavaFriendlyAPI + private suspend fun getItemsSequence(): Sequence { + return sequence { + var index = 0 + while (true) { + val list = runBlocking { + FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = id, + startIndex = index + ).sendAndExpect(bot) + }.toResult("AbsoluteFolderImpl.getFilesFlow").getOrThrow() + index += list.itemList.size + + if (list.int32RetCode != 0) return@sequence + if (list.itemList.isEmpty()) return@sequence + + yieldAll(list.itemList) + } + } + } + + private fun Oidb0x6d8.GetFileListRspBody.Item.resolve(): AbsoluteFileFolder? = resolved(this@AbsoluteFolderImpl) + + override suspend fun folders(): Flow { + return getItemsFlow().filter { it.folderInfo != null }.map { it.resolve() as AbsoluteFolder } + } + + @JavaFriendlyAPI + override suspend fun foldersStream(): Stream { + return getItemsSequence().filter { it.folderInfo != null }.map { it.resolve() as AbsoluteFolder }.asStream() + } + + override suspend fun files(): Flow { + return getItemsFlow().filter { it.fileInfo != null }.map { it.resolve() as AbsoluteFile } + } + + @JavaFriendlyAPI + override suspend fun filesStream(): Stream { + return getItemsSequence().filter { it.fileInfo != null }.map { it.resolve() as AbsoluteFile }.asStream() + } + + override suspend fun children(): Flow { + return getItemsFlow().mapNotNull { it.resolve() } + } + + @JavaFriendlyAPI + override suspend fun childrenStream(): Stream { + return getItemsSequence().mapNotNull { it.resolve() }.asStream() + } + + override suspend fun createFolder(name: String): AbsoluteFolder { + if (name.isBlank()) throw IllegalArgumentException("folder name cannot be blank.") + checkPermission("createFolder") + FileSystem.checkLegitimacy(name) + + // server only support nesting depth level of 1 so we don't need to check the name + + val result = FileManagement.CreateFolder(client, contact.id, this.id, name) + .sendAndExpect(bot).toResult("AbsoluteFolderImpl.mkdir", checkResp = false) + .getOrThrow() // throw protocol errors + + /* + 2021-10-30 13:06:33 D/soutv: unnamed = CreateFolderRspBody#-941698272 { + folderInfo=FolderInfo#1879610684 { + createTime=0x617D3548(1635595592) + createUin=xxx + folderId=/49a18e46-cf24-4362-b0d0-13235c0e7862 + folderName=myFolder + modifyTime=0x617D3548(1635595592) + modifyUin=xxx + parentFolderId=/ + usedSpace=0x0000000000000000(0) + } + retMsg=ok + } + */ + + /* + 2021-10-30 13:03:44 D/soutv: unnamed = CreateFolderRspBody#-941698272 { + clientWording=只允许群主和管理员操作 + int32RetCode=0xFFFFFFDC(-36) + retMsg=not group admin + } + */ + + /* + 2021-10-30 13:10:32 D/soutv: unnamed = CreateFolderRspBody#-941698272 { + clientWording=同名文件夹已存在 + int32RetCode=0xFFFFFEC7(-313) + retMsg=folder name has exist + } + */ + + return when (result.int32RetCode) { + -36 -> throwPermissionDeniedException("createFolder") + -313 -> this.resolveFolder(name) // already exists + 0 -> { + if (result.folderInfo != null) { + this.createChildFolder(result.folderInfo) + } else { + this.resolveFolder(name) + } + } + else -> { + // unexpected errors + error("Failed to create folder '$name': ${result.int32RetCode} ${result.clientWording}.") + } + } ?: error("Failed to create folder '$name': server returned success but failed to find folder.") + } + + override suspend fun resolveFolder(name: String): AbsoluteFolder? { + if (name.isBlank()) throw IllegalArgumentException("folder name cannot be blank.") + if (!FileSystem.isLegal(name)) return null + return getItemsFlow().firstOrNull { it.folderInfo?.folderName == name }?.resolve() as AbsoluteFolder? + } + + override suspend fun resolveFileById(id: String, deep: Boolean): AbsoluteFile? { + if (id == "/" || id.isEmpty()) throw IllegalArgumentException("Illegal file id: $id") + getItemsFlow().filter { it.fileInfo?.fileId == id }.map { it.resolve() as AbsoluteFile }.firstOrNull() + ?.let { return it } + + if (!deep) return null + + return folders().map { it.resolveFileById(id, deep) }.firstOrNull() + } + + override suspend fun resolveFiles(path: String): Flow { + if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.") + if (!FileSystem.isLegal(path)) return emptyFlow() + + if (!path.contains('/')) { + return getItemsFlow().filter { it.fileInfo?.fileName == path }.map { it.resolve() as AbsoluteFile } + } + + return resolveFolder(path.substringBefore('/'))?.resolveFiles(path.substringAfter('/')) ?: emptyFlow() + } + + @OptIn(JavaFriendlyAPI::class) + override suspend fun resolveFilesStream(path: String): Stream { + if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.") + if (!FileSystem.isLegal(path)) return Stream.empty() + + if (!path.contains('/')) { + return getItemsSequence().filter { it.fileInfo?.fileName == path }.map { it.resolve() as AbsoluteFile } + .asStream() + } + + return resolveFolder(path.substringBefore('/'))?.resolveFilesStream(path.substringAfter('/')) ?: Stream.empty() + } + + override suspend fun resolveAll(path: String): Flow { + if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.") + if (!FileSystem.isLegal(path)) return emptyFlow() + if (!path.contains('/')) { + return getItemsFlow().mapNotNull { it.resolve() } + } + + return resolveFolder(path.substringBefore('/'))?.resolveAll(path.substringAfter('/')) ?: emptyFlow() + } + + @JavaFriendlyAPI + override suspend fun resolveAllStream(path: String): Stream { + if (path.isBlank()) throw IllegalArgumentException("path cannot be blank.") + if (!FileSystem.isLegal(path)) return Stream.empty() + if (!path.contains('/')) { + return getItemsSequence().mapNotNull { it.resolve() }.asStream() + } + + return resolveFolder(path.substringBefore('/'))?.resolveAllStream(path.substringAfter('/')) ?: Stream.empty() + } + + override suspend fun uploadNewFile( + filepath: String, + content: ExternalResource, + callback: ProgressionCallback? + ): AbsoluteFile { + val (actualFolder, actualFilename) = findFileByPath(filepath) + return uploadNewFileImpl(actualFolder.impl(), actualFilename, content, callback) + } + + override suspend fun exists(): Boolean { + return parentOrFail().folders().firstOrNull { it.id == this.id } != null + } + + override suspend fun refresh(): Boolean { + val new = refreshed() ?: return false + this.name = new.name + this.lastModifiedTime = new.lastModifiedTime + this.contentsCount = new.contentsCount + return true + } + + override fun toString(): String = "AbsoluteFolder(name=$name, absolutePath=$absolutePath, id=$id)" + + override suspend fun refreshed(): AbsoluteFolder? = parentOrRoot.folders().firstOrNull { it.id == this.id } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as AbsoluteFolderImpl + + if (contentsCount != other.contentsCount) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + contentsCount.hashCode() + return result + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/file/AbstractAbsoluteFileFolder.kt b/mirai-core/src/commonMain/kotlin/contact/file/AbstractAbsoluteFileFolder.kt new file mode 100644 index 000000000..58cca374f --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/file/AbstractAbsoluteFileFolder.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2019-2021 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("MemberVisibilityCanBePrivate") + +package net.mamoe.mirai.internal.contact.file + +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.PermissionDeniedException +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.contact.file.AbsoluteFileFolder +import net.mamoe.mirai.contact.file.AbsoluteFolder +import net.mamoe.mirai.internal.asQQAndroidBot +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.network.protocol.packet.sendAndExpect +import net.mamoe.mirai.internal.utils.FileSystem +import net.mamoe.mirai.utils.cast + +internal fun AbstractAbsoluteFileFolder.api(): AbsoluteFileFolder = this.cast() +internal fun AbsoluteFileFolder.impl(): AbstractAbsoluteFileFolder = this.cast() +internal fun AbsoluteFile.impl(): AbsoluteFileImpl = this.cast() +internal fun AbsoluteFolder.impl(): AbsoluteFolderImpl = this.cast() + +internal val AbsoluteFolder?.idOrRoot get() = this?.id ?: AbsoluteFolder.ROOT_FOLDER_ID + +internal val AbstractAbsoluteFileFolder.parentOrRoot get() = parent ?: contact.files.root + +/** + * @see AbsoluteFileFolder + */ +internal abstract class AbstractAbsoluteFileFolder( + // overriding AbsFileFolder + val contact: FileSupported, + var parent: AbsoluteFolder?, + val id: String, // uuid-like + var name: String, + val uploadTime: Long, + val uploaderId: Long, + var lastModifiedTime: Long, + // end + + val busId: Int, // protocol internal +) { + protected inline val bot get() = contact.bot.asQQAndroidBot() + protected inline val client get() = bot.client + + protected abstract fun checkPermission(operationHint: String) + + fun throwPermissionDeniedException(operationHint: String): Nothing { + throw PermissionDeniedException("Permission denied: '$operationHint' on file '${this.api().absolutePath}' requires an operator permission.") + } + + protected fun parentOrFail() = parent ?: error("Cannot rename the root folder.") + + /////////////////////////////////////////////////////////////////////////// + // overriding AbsFileFolder + /////////////////////////////////////////////////////////////////////////// + + protected abstract val isFile: Boolean + protected abstract val isFolder: Boolean + + suspend fun renameTo(newName: String): Boolean { + FileSystem.checkLegitimacy(newName) + parentOrFail() + checkPermission("renameTo") + + val result = if (isFile) { + FileManagement.RenameFile(client, contact.id, busId, id, parent.idOrRoot, newName) + } else { + FileManagement.RenameFolder(client, contact.id, id, newName) + }.sendAndExpect(bot) + + result.toResult("AbstractAbsoluteFileFolder.renameTo") { + when (it) { + 0 -> { + name = newName + return true + } + 1 -> return false + else -> false + } + }.getOrThrow() + + error("unreachable") + } + + suspend fun delete(): Boolean { + checkPermission("delete") + val result = if (isFile) { + FileManagement.DeleteFile(client, contact.id, busId, id, parent.idOrRoot).sendAndExpect(bot) + } else { + // natively 'recursive' + FileManagement.DeleteFolder(client, contact.id, id).sendAndExpect(bot) + }.toResult("AbstractAbsoluteFileFolder.delete", checkResp = false).getOrThrow() + + return when (result.int32RetCode) { + -36 -> throwPermissionDeniedException("delete") + 0 -> true + else -> { + // files not exists or other errors. + false + } + } + } + + @Suppress("DuplicatedCode") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AbstractAbsoluteFileFolder + + if (contact != other.contact) return false + if (parent != other.parent) return false + if (id != other.id) return false + if (name != other.name) return false + if (uploadTime != other.uploadTime) return false + if (uploaderId != other.uploaderId) return false + if (lastModifiedTime != other.lastModifiedTime) return false + if (busId != other.busId) return false + if (isFile != other.isFile) return false + if (isFolder != other.isFolder) return false + + return true + } + + override fun hashCode(): Int { + var result = contact.hashCode() + result = 31 * result + (parent?.hashCode() ?: 0) + result = 31 * result + id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + uploadTime.hashCode() + result = 31 * result + uploaderId.hashCode() + result = 31 * result + lastModifiedTime.hashCode() + result = 31 * result + busId + result = 31 * result + isFile.hashCode() + result = 31 * result + isFolder.hashCode() + return result + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/file/FileProtocol.kt b/mirai-core/src/commonMain/kotlin/contact/file/FileProtocol.kt new file mode 100644 index 000000000..b1a48bf30 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/file/FileProtocol.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2021 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.file + +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x6d6 +import net.mamoe.mirai.internal.network.protocol.packet.chat.CommonOidbResponse +import net.mamoe.mirai.internal.utils.FileSystem + +/** + * Abstract protocol bridge for file management. + */ +internal interface FileProtocol { + val fs: FileSystem get() = FileSystem + + fun renameFile( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + parentFolderId: String, + newName: String, + ): CommonOidbResponse +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/file/RemoteFilesImpl.kt b/mirai-core/src/commonMain/kotlin/contact/file/RemoteFilesImpl.kt new file mode 100644 index 000000000..05d2475a4 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/contact/file/RemoteFilesImpl.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019-2021 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.file + +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFolder +import net.mamoe.mirai.contact.file.RemoteFiles +import net.mamoe.mirai.internal.contact.ContactAware +import net.mamoe.mirai.internal.utils.FileSystem + +internal class RemoteFilesImpl( + override val contact: FileSupported, + override val root: AbsoluteFolder = AbsoluteFolderImpl( + contact, + null, + AbsoluteFolder.ROOT_FOLDER_ID, + "/", + 0, + 0, + 0, + 0 + ), +) : RemoteFiles, ContactAware { + companion object { + suspend fun AbsoluteFolder.findFileByPath(path: String): Pair { + if (path.isBlank()) throw IllegalArgumentException("absolutePath cannot be blank.") + val normalized = FileSystem.normalize(path) +// if (!normalized.contains('/')) { +// throw IllegalArgumentException("Invalid absolutePath: '$path'. If you wanted to upload file to root directory, please add a leading '/'.") +// } + val folder = when (normalized.count { it == '/' }) { + 0, 1 -> this + else -> this.createFolder(normalized.substringBeforeLast("/")) + } + + val filename = normalized.substringAfterLast('/') + return folder to filename + } + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt b/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt index 24cadb8c0..ed32d7199 100644 --- a/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt @@ -10,9 +10,26 @@ package net.mamoe.mirai.internal.message +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.onEach import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.contact.file.AbsoluteFile +import net.mamoe.mirai.contact.file.AbsoluteFolder +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.contact.file.AbsoluteFolderImpl +import net.mamoe.mirai.internal.contact.file.createChildFile +import net.mamoe.mirai.internal.contact.file.impl +import net.mamoe.mirai.internal.contact.file.resolved +import net.mamoe.mirai.internal.network.protocol.data.proto.Oidb0x6d8.GetFileListRspBody +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.network.protocol.packet.sendAndExpect import net.mamoe.mirai.message.data.FileMessage +import net.mamoe.mirai.utils.cast import kotlin.contracts.contract internal fun FileMessage.checkIsImpl(): FileMessageImpl { @@ -31,5 +48,41 @@ internal data class FileMessageImpl( override val internalId: Int get() = busId + override suspend fun toAbsoluteFile(contact: FileSupported): AbsoluteFile? { + val result = FileManagement.GetFileInfo(contact.bot.asQQAndroidBot().client, contact.id, id, busId) + .sendAndExpect(contact.bot.asQQAndroidBot()) + .toResult("FileMessage.toAbsoluteFile").getOrThrow() + if (result.fileInfo == null) return null + + // Get its parent AbsoluteFolder + // This is necessary for properties like creationTime. + // Maybe we can optimize it in the future (i.e. make it lazy?) + + val root = contact.files.root.impl() + val folder = if (result.fileInfo.parentFolderId == AbsoluteFolder.ROOT_FOLDER_ID) { + root + } else { + val folders = ArrayList() + root.impl().getItemsFlow() + .filter { it.folderInfo != null } + .onEach { folders.add(it) } + .firstOrNull { it.folderInfo?.folderId == result.fileInfo.parentFolderId } + ?.resolved(root) as AbsoluteFolderImpl? + ?: kotlin.run { + for (folder in folders) { + AbsoluteFolderImpl.getItemsFlow( + (contact.bot as QQAndroidBot).client, + contact, + folder.folderInfo!!.folderId + ).firstOrNull { it.folderInfo?.folderId == result.fileInfo.parentFolderId } + ?.resolved(root)?.cast()?.let { return@run it } + } + root + } + } + + return folder.createChildFile(result.fileInfo) + } + override fun toString(): String = "[mirai:file:$name,$id,$size,$busId]" } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt index 1a0bbbbbb..0a57e4815 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt @@ -73,6 +73,28 @@ internal inline fun CommonOidbResponse.toResult(actionName: String, check } } +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +internal inline fun CommonOidbResponse.toResult( + actionName: String, + checkResp: CheckableStruct.(Int) -> Boolean +): Result { + return if (this is CommonOidbResponse.Failure) { + Result.failure(this.createException(actionName)) + } else { + this as CommonOidbResponse.Success + val result = this.resp + if (result is CheckableStruct) { + if (!checkResp( + result, + result.int32RetCode + ) + ) return Result.failure(IllegalStateException("Failed $actionName, result=${result.int32RetCode}, msg=${result.retMsg}")) + } + Result.success(this.resp) + } +} + /** * @param respMapper may throw any exception, which will be wrapped to CommonOidbResponse.Failure */ diff --git a/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt b/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt index 7be76f52e..a7b779400 100644 --- a/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt +++ b/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt @@ -7,6 +7,8 @@ * https://github.com/mamoe/mirai/blob/dev/LICENSE */ +@file:Suppress("DEPRECATION", "OverridingDeprecatedMember") + package net.mamoe.mirai.internal.utils import kotlinx.coroutines.flow.* @@ -46,6 +48,10 @@ internal object FileSystem { } } + fun isLegal(path: String): Boolean { + return path.firstOrNull { it in """:*?"<>|""" } == null + } + fun normalize(path: String): String { checkLegitimacy(path) return path.replace('\\', '/')