From e256ec06d3b23f3183ab7eba4b8e730ce5eba397 Mon Sep 17 00:00:00 2001 From: Him188 Date: Mon, 8 Mar 2021 18:34:45 +0800 Subject: [PATCH] Support file operations (#1069) * Proto structs for group file * RemoteFile fundamental abstraction and proto structs * Configure JVM target for mirai-console-intellij * Add Group.filesRoot * Fix build * Implement a FileSystem for RemoteFile resolution * Fix RemoteFile FileSystem and implement resolve and listFiles * Implement file info query and file download * Support uploading group file * Support file feeds * 2.5-M2-dev-1 * Fix tests * 2.5-M2-dev-2 * Add uuid-based resolving, support getting file details * Support FileMessage receive * Support sending FileMessage * 2.5-M2-dev-3 * Add DownloadInfo.id * Improve RemoteFile.delete * Support move, delete, rename, mkdir. Simplify listFiles * - Rename RemoteFile.write to .upload. - Prefer id matching - Improve move * Add permission checks * Improve permission checks * Rearrange functions and add constant ROOT_PATH * Introduce FileSupported, add extensions * Introduce ProgressionCallback * Fix docs and uploadFileAndSend * Remove empty FileHighway.kt * Add test testNormalize * Add RemoteFile.contact, change RemoteFile.uploadAndSend return type to MessageReceipt * Move @JvmBlockingBridge to file * Change FileMessage.toRemoteFile parameter type Group to FileSupported * Add impl notes #1082 --- ...binary-compatibility-validator-android.api | 144 ++++- .../api/binary-compatibility-validator.api | 144 ++++- buildSrc/src/main/kotlin/Versions.kt | 2 +- mirai-core-api/build.gradle.kts | 2 +- .../kotlin/contact/FileSupported.kt | 29 + .../src/commonMain/kotlin/contact/Group.kt | 3 +- .../internal/utils/ExternalImageImpls.kt | 8 +- .../kotlin/message/data/FileMessage.kt | 50 ++ .../kotlin/utils/ExternalResource.kt | 36 +- .../src/commonMain/kotlin/utils/RemoteFile.kt | 390 +++++++++++++ .../src/androidMain/kotlin/Actuals.kt | 6 +- .../src/commonMain/kotlin/Bytes.kt | 3 +- .../commonMain/kotlin/MiraiPlatformUtils.kt | 18 +- .../src/jvmMain/kotlin/Actuals.kt | 6 +- mirai-core/src/commonMain/kotlin/MiraiImpl.kt | 1 + .../commonMain/kotlin/contact/GroupImpl.kt | 3 + .../kotlin/contact/SendMessageHandler.kt | 60 +- .../src/commonMain/kotlin/contact/util.kt | 118 ---- .../kotlin/message/FileMessageImpl.kt | 55 ++ .../kotlin/message/ReceiveMessageHandler.kt | 62 +- .../kotlin/message/incomingSourceImpl.kt | 4 +- .../network/highway/ChunkedFlowSession.kt | 4 +- .../kotlin/network/highway/Highway.kt | 94 ++- .../network/protocol/data/proto/Exciting.kt | 81 +++ .../protocol/data/proto/GroupFileCommon.kt | 104 ++++ .../network/protocol/data/proto/Highway.kt | 12 +- .../protocol/data/proto/HummerResv21.kt | 5 +- .../kotlin/network/protocol/data/proto/Msg.kt | 6 +- .../network/protocol/data/proto/OIDB.kt | 2 +- .../network/protocol/data/proto/Oidb0x6d6.kt | 179 ++++++ .../network/protocol/data/proto/Oidb0x6d7.kt | 99 ++++ .../network/protocol/data/proto/Oidb0x6d8.kt | 168 ++++++ .../network/protocol/data/proto/Oidb0x6d9.kt | 120 ++++ .../network/protocol/packet/PacketFactory.kt | 1 + .../network/protocol/packet/chat/GroupFile.kt | 502 ++++++++++++++++ .../protocol/packet/chat/TroopManagement.kt | 3 +- .../chat/receive/MessageSvc.PbSendMsg.kt | 27 +- .../chat/receive/OnlinePush.PbPushGroupMsg.kt | 6 +- .../commonMain/kotlin/utils/PlatformSocket.kt | 7 +- .../commonMain/kotlin/utils/RemoteFileImpl.kt | 537 ++++++++++++++++++ .../kotlin/utils/io/serialization/utils.kt | 65 +++ .../kotlin/utils/retryWithServers.kt | 9 +- .../kotlin/internal/utils/FileSystemTest.kt | 40 ++ 43 files changed, 3000 insertions(+), 215 deletions(-) create mode 100644 mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt create mode 100644 mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt create mode 100644 mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Exciting.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/GroupFileCommon.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d6.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d7.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d8.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d9.kt create mode 100644 mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt create mode 100644 mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt create mode 100644 mirai-core/src/commonTest/kotlin/internal/utils/FileSystemTest.kt 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 4b282396c..5b2524b23 100644 --- a/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api +++ b/binary-compatibility-validator/android/api/binary-compatibility-validator-android.api @@ -341,6 +341,10 @@ public final class net/mamoe/mirai/contact/ExceptionsKt { public static final fun getBotMuteRemaining (Lnet/mamoe/mirai/contact/BotIsBeingMutedException;)I } +public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact { + public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; +} + public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/User { public synthetic fun delete ()Lkotlin/Unit; public fun delete ()V @@ -357,7 +361,7 @@ public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/corouti public abstract fun sendMessage (Lnet/mamoe/mirai/message/data/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact { +public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported { public static final field Companion Lnet/mamoe/mirai/contact/Group$Companion; public abstract fun contains (J)Z public fun contains (Lnet/mamoe/mirai/contact/NormalMember;)Z @@ -4038,6 +4042,23 @@ public final class net/mamoe/mirai/message/data/Face$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/FileMessage$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public fun contentToString ()Ljava/lang/String; + public abstract fun getId ()Ljava/lang/String; + public fun getKey ()Lnet/mamoe/mirai/message/data/FileMessage$Key; + public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getSize ()J + 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; +} + +public final class net/mamoe/mirai/message/data/FileMessage$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public final class net/mamoe/mirai/message/data/FlashImage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/HummerMessage, net/mamoe/mirai/message/data/MessageContent { public static final field Key Lnet/mamoe/mirai/message/data/FlashImage$Key; public static final field SERIAL_NAME Ljava/lang/String; @@ -5611,6 +5632,7 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public abstract fun getClosed ()Lkotlinx/coroutines/Deferred; public abstract fun getFormatName ()Ljava/lang/String; public abstract fun getMd5 ()[B + public fun getSha1 ()[B public abstract fun getSize ()J public abstract fun inputStream ()Ljava/io/InputStream; public static fun sendAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/MessageReceipt; @@ -5625,6 +5647,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static fun sendAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/MessageReceipt; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/MessageReceipt; + public static fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -5639,6 +5663,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class net/mamoe/mirai/utils/ExternalResource$Companion { @@ -5668,6 +5694,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public final fun sendAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -5682,6 +5710,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface class net/mamoe/mirai/utils/FileCacheStrategy { @@ -5838,6 +5868,118 @@ public class net/mamoe/mirai/utils/PlatformLogger : net/mamoe/mirai/utils/MiraiL public fun warning0 (Ljava/lang/String;Ljava/lang/Throwable;)V } +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; + 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 getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public fun getDownloadInfo ()Lnet/mamoe/mirai/utils/RemoteFile$DownloadInfo; + public abstract fun getDownloadInfo (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getId ()Ljava/lang/String; + public fun getInfo ()Lnet/mamoe/mirai/utils/RemoteFile$FileInfo; + public abstract fun getInfo (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getParent ()Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun getPath ()Ljava/lang/String; + public fun isDirectory ()Z + public fun isDirectory (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun isFile ()Z + public abstract fun isFile (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun length ()J + public abstract fun length (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun listFiles ()Lkotlinx/coroutines/flow/Flow; + public abstract fun listFiles (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun listFilesIterator (Z)Ljava/util/Iterator; + public abstract fun listFilesIterator (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun mkdir ()Z + public abstract fun mkdir (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Ljava/lang/String;)Z + public abstract fun moveTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Lnet/mamoe/mirai/utils/RemoteFile;)Z + public abstract fun moveTo (Lnet/mamoe/mirai/utils/RemoteFile;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 resolve (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolve (Lnet/mamoe/mirai/utils/RemoteFile;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun resolveById (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun resolveById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveById (Ljava/lang/String;Z)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolveById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun resolveById$default (Lnet/mamoe/mirai/utils/RemoteFile;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun resolveSibling (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolveSibling (Lnet/mamoe/mirai/utils/RemoteFile;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage; + public abstract fun toMessage (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toString ()Ljava/lang/String; + public fun upload (Ljava/io/File;)Z + public fun upload (Ljava/io/File;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;)Z + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Z + public abstract fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun upload$default (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun uploadAndSend (Ljava/io/File;)Lnet/mamoe/mirai/message/MessageReceipt; + public fun uploadAndSend (Ljava/io/File;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadAndSend (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public abstract fun uploadAndSend (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public static fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/utils/RemoteFile$Companion { + public static final field ROOT_PATH Ljava/lang/String; + public final fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public final fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/utils/RemoteFile$DownloadInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B[B)V + public final fun getFilename ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getMd5 ()[B + public final fun getPath ()Ljava/lang/String; + public final fun getSha1 ()[B + public final fun getUrl ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/utils/RemoteFile$FileInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JIJJJ[B[B)V + public final fun getDownloadTimes ()I + public final fun getId ()Ljava/lang/String; + public final fun getLastModifyTime ()J + public final fun getLength ()J + public final fun getMd5 ()[B + public final fun getName ()Ljava/lang/String; + public final fun getPath ()Ljava/lang/String; + public final fun getSha1 ()[B + public final fun getUploadTime ()J + public final fun getUploaderId ()J + public final fun resolveToFile (Lnet/mamoe/mirai/contact/Group;)Lnet/mamoe/mirai/utils/RemoteFile; + public final fun resolveToFile (Lnet/mamoe/mirai/contact/Group;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class net/mamoe/mirai/utils/RemoteFile$ProgressionCallback { + public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion; + public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; + public fun onBegin (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;)V + public fun onFailure (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V + public fun onProgression (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;J)V + public fun onSuccess (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;)V +} + +public final class net/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion { + public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; + public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; +} + public final class net/mamoe/mirai/utils/SilentLogger : net/mamoe/mirai/utils/PlatformLogger { public static final field INSTANCE Lnet/mamoe/mirai/utils/SilentLogger; public fun debug0 (Ljava/lang/String;)V diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index eff5be96f..bf5c11d9b 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -341,6 +341,10 @@ public final class net/mamoe/mirai/contact/ExceptionsKt { public static final fun getBotMuteRemaining (Lnet/mamoe/mirai/contact/BotIsBeingMutedException;)I } +public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamoe/mirai/contact/Contact { + public abstract fun getFilesRoot ()Lnet/mamoe/mirai/utils/RemoteFile; +} + public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/User { public synthetic fun delete ()Lkotlin/Unit; public fun delete ()V @@ -357,7 +361,7 @@ public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/corouti public abstract fun sendMessage (Lnet/mamoe/mirai/message/data/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact { +public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/Contact, net/mamoe/mirai/contact/FileSupported { public static final field Companion Lnet/mamoe/mirai/contact/Group$Companion; public abstract fun contains (J)Z public fun contains (Lnet/mamoe/mirai/contact/NormalMember;)Z @@ -4038,6 +4042,23 @@ public final class net/mamoe/mirai/message/data/Face$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } +public abstract interface class net/mamoe/mirai/message/data/FileMessage : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent { + public static final field Key Lnet/mamoe/mirai/message/data/FileMessage$Key; + public static final field SERIAL_NAME Ljava/lang/String; + public fun contentToString ()Ljava/lang/String; + public abstract fun getId ()Ljava/lang/String; + public fun getKey ()Lnet/mamoe/mirai/message/data/FileMessage$Key; + public synthetic fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getSize ()J + 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; +} + +public final class net/mamoe/mirai/message/data/FileMessage$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey { + public static final field SERIAL_NAME Ljava/lang/String; +} + public final class net/mamoe/mirai/message/data/FlashImage : net/mamoe/mirai/message/code/CodableMessage, net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/HummerMessage, net/mamoe/mirai/message/data/MessageContent { public static final field Key Lnet/mamoe/mirai/message/data/FlashImage$Key; public static final field SERIAL_NAME Ljava/lang/String; @@ -5611,6 +5632,7 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public abstract fun getClosed ()Lkotlinx/coroutines/Deferred; public abstract fun getFormatName ()Ljava/lang/String; public abstract fun getMd5 ()[B + public fun getSha1 ()[B public abstract fun getSize ()J public abstract fun inputStream ()Ljava/io/InputStream; public static fun sendAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/MessageReceipt; @@ -5625,6 +5647,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static fun sendAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/MessageReceipt; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/MessageReceipt; + public static fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -5639,6 +5663,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/message/data/Image; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class net/mamoe/mirai/utils/ExternalResource$Companion { @@ -5668,6 +5694,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public final fun sendAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static synthetic fun sendAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public final fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadAsFileTo (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image; public final fun uploadAsImage (Ljava/io/File;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -5682,6 +5710,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion { public static synthetic fun uploadAsImage$default (Lnet/mamoe/mirai/utils/ExternalResource$Companion;Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Voice; public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface class net/mamoe/mirai/utils/FileCacheStrategy { @@ -5867,6 +5897,118 @@ protected final class net/mamoe/mirai/utils/PlatformLogger$Color : java/lang/Enu public static fun values ()[Lnet/mamoe/mirai/utils/PlatformLogger$Color; } +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; + 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 getContact ()Lnet/mamoe/mirai/contact/FileSupported; + public fun getDownloadInfo ()Lnet/mamoe/mirai/utils/RemoteFile$DownloadInfo; + public abstract fun getDownloadInfo (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getId ()Ljava/lang/String; + public fun getInfo ()Lnet/mamoe/mirai/utils/RemoteFile$FileInfo; + public abstract fun getInfo (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun getName ()Ljava/lang/String; + public abstract fun getParent ()Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun getPath ()Ljava/lang/String; + public fun isDirectory ()Z + public fun isDirectory (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun isFile ()Z + public abstract fun isFile (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun length ()J + public abstract fun length (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun listFiles ()Lkotlinx/coroutines/flow/Flow; + public abstract fun listFiles (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun listFilesIterator (Z)Ljava/util/Iterator; + public abstract fun listFilesIterator (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun mkdir ()Z + public abstract fun mkdir (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Ljava/lang/String;)Z + public abstract fun moveTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun moveTo (Lnet/mamoe/mirai/utils/RemoteFile;)Z + public abstract fun moveTo (Lnet/mamoe/mirai/utils/RemoteFile;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 resolve (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolve (Lnet/mamoe/mirai/utils/RemoteFile;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun resolveById (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun resolveById (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun resolveById (Ljava/lang/String;Z)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolveById (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun resolveById$default (Lnet/mamoe/mirai/utils/RemoteFile;Ljava/lang/String;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public abstract fun resolveSibling (Ljava/lang/String;)Lnet/mamoe/mirai/utils/RemoteFile; + public abstract fun resolveSibling (Lnet/mamoe/mirai/utils/RemoteFile;)Lnet/mamoe/mirai/utils/RemoteFile; + public fun toMessage ()Lnet/mamoe/mirai/message/data/FileMessage; + public abstract fun toMessage (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun toString ()Ljava/lang/String; + public fun upload (Ljava/io/File;)Z + public fun upload (Ljava/io/File;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;)Z + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Z + public abstract fun upload (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun upload$default (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; + public fun uploadAndSend (Ljava/io/File;)Lnet/mamoe/mirai/message/MessageReceipt; + public fun uploadAndSend (Ljava/io/File;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun uploadAndSend (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public abstract fun uploadAndSend (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/FileMessage; + public static fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public static fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/utils/RemoteFile$Companion { + public static final field ROOT_PATH Ljava/lang/String; + public final fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/FileMessage; + public final fun uploadFile (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/MessageReceipt; + public final fun uploadFileAndSend (Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class net/mamoe/mirai/utils/RemoteFile$DownloadInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[B[B)V + public final fun getFilename ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getMd5 ()[B + public final fun getPath ()Ljava/lang/String; + public final fun getSha1 ()[B + public final fun getUrl ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class net/mamoe/mirai/utils/RemoteFile$FileInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JIJJJ[B[B)V + public final fun getDownloadTimes ()I + public final fun getId ()Ljava/lang/String; + public final fun getLastModifyTime ()J + public final fun getLength ()J + public final fun getMd5 ()[B + public final fun getName ()Ljava/lang/String; + public final fun getPath ()Ljava/lang/String; + public final fun getSha1 ()[B + public final fun getUploadTime ()J + public final fun getUploaderId ()J + public final fun resolveToFile (Lnet/mamoe/mirai/contact/Group;)Lnet/mamoe/mirai/utils/RemoteFile; + public final fun resolveToFile (Lnet/mamoe/mirai/contact/Group;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public abstract interface class net/mamoe/mirai/utils/RemoteFile$ProgressionCallback { + public static final field Companion Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion; + public static fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; + public fun onBegin (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;)V + public fun onFailure (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;Ljava/lang/Throwable;)V + public fun onProgression (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;J)V + public fun onSuccess (Lnet/mamoe/mirai/utils/RemoteFile;Lnet/mamoe/mirai/utils/ExternalResource;)V +} + +public final class net/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion { + public final fun asProgressionCallback (Lkotlinx/coroutines/channels/SendChannel;Z)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; + public static synthetic fun asProgressionCallback$default (Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback$Companion;Lkotlinx/coroutines/channels/SendChannel;ZILjava/lang/Object;)Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback; +} + public final class net/mamoe/mirai/utils/SilentLogger : net/mamoe/mirai/utils/PlatformLogger { public static final field INSTANCE Lnet/mamoe/mirai/utils/SilentLogger; public fun debug0 (Ljava/lang/String;)V diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index ba532c75f..de5aaf052 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -12,7 +12,7 @@ import org.gradle.api.attributes.Attribute object Versions { - const val project = "2.5-M1" + const val project = "2.5-M2-dev-3" const val core = project const val console = project diff --git a/mirai-core-api/build.gradle.kts b/mirai-core-api/build.gradle.kts index e9b7fb7f7..3efab5f9d 100644 --- a/mirai-core-api/build.gradle.kts +++ b/mirai-core-api/build.gradle.kts @@ -60,7 +60,7 @@ kotlin { api(`kotlinx-serialization-core`) api(`kotlinx-serialization-json`) implementation(`kotlinx-serialization-protobuf`) - api(`kotlinx-coroutines-core`) + api(`kotlinx-coroutines-jdk8`) implementation(`jetbrains-annotations`) // api(`kotlinx-coroutines-jdk8`) diff --git a/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt new file mode 100644 index 000000000..0d6391c3d --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/contact/FileSupported.kt @@ -0,0 +1,29 @@ +/* + * 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/master/LICENSE + */ + + +package net.mamoe.mirai.contact + +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.RemoteFile + +/** + * 支持文件操作的 [Contact]. 目前仅 [Group] + * @since 2.5 + */ +@MiraiExperimentalApi +public interface FileSupported : Contact { + /** + * 文件根目录. 可通过 [RemoteFile.listFiles] 获取目录下文件列表. + * + * @since 2.5 + */ + @MiraiExperimentalApi + public val filesRoot: RemoteFile +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt index e0714f18d..d597328f6 100644 --- a/mirai-core-api/src/commonMain/kotlin/contact/Group.kt +++ b/mirai-core-api/src/commonMain/kotlin/contact/Group.kt @@ -26,7 +26,7 @@ import net.mamoe.mirai.utils.OverFileSizeMaxException * 群. */ @JvmBlockingBridge -public interface Group : Contact, CoroutineScope { +public interface Group : Contact, CoroutineScope, FileSupported { /** * 群名称. * @@ -83,6 +83,7 @@ public interface Group : Contact, CoroutineScope { public override val avatarUrl: String get() = "https://p.qlogo.cn/gh/$id/${id}/640" + /** * 群成员列表, 不含机器人自己, 含群主. * diff --git a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt index 7a2d8258a..84aff7c6d 100644 --- a/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt +++ b/mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalImageImpls.kt @@ -10,10 +10,7 @@ package net.mamoe.mirai.internal.utils import kotlinx.coroutines.CompletableDeferred -import net.mamoe.mirai.utils.COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE -import net.mamoe.mirai.utils.ExternalResource -import net.mamoe.mirai.utils.getFileType -import net.mamoe.mirai.utils.md5 +import net.mamoe.mirai.utils.* import java.io.InputStream import java.io.RandomAccessFile @@ -31,6 +28,7 @@ internal class ExternalResourceImplByFileWithMd5( override val md5: ByteArray, formatName: String? ) : ExternalResource { + override val sha1: ByteArray by lazy { inputStream().sha1() } override val size: Long = file.length() override val formatName: String by lazy { formatName ?: inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME @@ -59,6 +57,7 @@ internal class ExternalResourceImplByFile( ) : ExternalResource { override val size: Long = file.length() override val md5: ByteArray by lazy { inputStream().md5() } + override val sha1: ByteArray by lazy { inputStream().sha1() } override val formatName: String by lazy { formatName ?: inputStream().detectFileTypeAndClose() ?: ExternalResource.DEFAULT_FORMAT_NAME } @@ -84,6 +83,7 @@ internal class ExternalResourceImplByByteArray( ) : ExternalResource { override val size: Long = data.size.toLong() override val md5: ByteArray by lazy { data.md5() } + override val sha1: ByteArray by lazy { data.sha1() } override val formatName: String by lazy { formatName ?: getFileType(data.copyOf(COUNT_BYTES_USED_FOR_DETECTING_FILE_TYPE)) ?: ExternalResource.DEFAULT_FORMAT_NAME diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt new file mode 100644 index 000000000..9bdb451e4 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/FileMessage.kt @@ -0,0 +1,50 @@ +/* + * 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/master/LICENSE + */ + + +package net.mamoe.mirai.message.data + +import kotlinx.serialization.SerialName +import net.mamoe.kjbb.JvmBlockingBridge +import net.mamoe.mirai.contact.FileSupported +import net.mamoe.mirai.utils.MiraiExperimentalApi +import net.mamoe.mirai.utils.RemoteFile +import net.mamoe.mirai.utils.safeCast + +/** + * 文件消息. + * + * @since 2.5 + * @suppress 文件消息不稳定, 可能在未来版本有不兼容变更. + */ +@SerialName(FileMessage.SERIAL_NAME) +@MiraiExperimentalApi +public interface FileMessage : MessageContent, ConstrainSingle { + public val name: String + public val id: String + public val size: Long + + override fun contentToString(): String = "[文件]$name" // orthodox + + /** + * 获取一个对应的 [RemoteFile]. 当目标群或好友不存在这个文件时返回 `null`. + */ + @MiraiExperimentalApi + @JvmBlockingBridge + public suspend fun toRemoteFile(contact: FileSupported): RemoteFile? { + return contact.filesRoot.resolveById(id) + } + + override val key: Key get() = Key + + public companion object Key : + AbstractPolymorphicMessageKey(MessageContent, { it.safeCast() }) { + public const val SERIAL_NAME: String = "FileMessage" + } +} \ No newline at end of file diff --git a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt index e32a64b34..6b03b6a69 100644 --- a/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt +++ b/mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt @@ -18,16 +18,19 @@ import net.mamoe.mirai.Mirai import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.contact.Contact.Companion.sendImage import net.mamoe.mirai.contact.Contact.Companion.uploadImage +import net.mamoe.mirai.contact.FileSupported import net.mamoe.mirai.contact.Group import net.mamoe.mirai.internal.utils.ExternalResourceImplByByteArray import net.mamoe.mirai.internal.utils.ExternalResourceImplByFile import net.mamoe.mirai.message.MessageReceipt +import net.mamoe.mirai.message.data.FileMessage import net.mamoe.mirai.message.data.Image import net.mamoe.mirai.message.data.Voice import net.mamoe.mirai.message.data.sendTo import net.mamoe.mirai.utils.ExternalResource.Companion.sendAsImageTo import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsImage +import net.mamoe.mirai.utils.RemoteFile.Companion.uploadFile import java.io.* @@ -44,7 +47,7 @@ import java.io.* * * ## 释放 * - * 当 [ExternalResource] 创建时就可能会打开个文件 (如使用 [File.toExternalResource]). + * 当 [ExternalResource] 创建时就可能会打开一个文件 (如使用 [File.toExternalResource]). * 类似于 [InputStream], [ExternalResource] 需要被 [关闭][close]. * * @see ExternalResource.uploadAsImage 将资源作为图片上传, 得到 [Image] @@ -61,6 +64,14 @@ public interface ExternalResource : Closeable { */ public val md5: ByteArray + /** + * 文件内容 SHA1. 16 bytes + * @since 2.5 + */ + public val sha1: ByteArray + get() = + throw UnsupportedOperationException("ExternalResource.sha1 is not implemented by ${this::class.simpleName}") + /** * 文件格式,如 "png", "amr". 当无法自动识别格式时为 [DEFAULT_FORMAT_NAME]. * @@ -252,6 +263,29 @@ public interface ExternalResource : Closeable { public suspend fun File.uploadAsImage(contact: Contact, formatName: String? = null): Image = toExternalResource(formatName).withUse { uploadAsImage(contact) } + /** + * 上传文件并获取文件消息. + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.upload + */ + @JvmStatic + @JvmBlockingBridge + public suspend fun File.uploadTo(contact: FileSupported, path: String): FileMessage = + toExternalResource().use { contact.uploadFile(path, it) } + + /** + * 上传文件并获取文件消息. 无论上传是否成功, 本函数都不会关闭资源. + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @since 2.5 + * @see RemoteFile.path + * @see RemoteFile.upload + */ + @JvmStatic + @JvmBlockingBridge + public suspend fun ExternalResource.uploadAsFileTo(contact: FileSupported, path: String): FileMessage = + contact.uploadFile(path, this) /** * 将文件作为语音上传后构造 [Voice]. diff --git a/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt new file mode 100644 index 000000000..fb1ee0e70 --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/utils/RemoteFile.kt @@ -0,0 +1,390 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused") +@file:JvmBlockingBridge + +package net.mamoe.mirai.utils + +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import net.mamoe.kjbb.JvmBlockingBridge +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.ExternalResource.Companion.toExternalResource +import java.io.File + +/** + * 表示一个远程文件或目录. + * + * @since 2.5 + */ +@MiraiExperimentalApi +public 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? + + /** + * 此文件所属的群或好友 + */ + @MiraiExperimentalApi + public val contact: FileSupported + + /** + * 当 [RemoteFile] 表示一个文件时返回 `true`. + */ + public suspend fun isFile(): Boolean + + /** + * 当 [RemoteFile] 表示一个目录时返回 `true`. + */ + public suspend fun isDirectory(): Boolean = !isFile() + + /** + * 获取文件长度. 当 [RemoteFile] 表示一个目录时行为不确定. + */ + public suspend fun length(): Long + + public class FileInfo @MiraiInternalApi constructor( + /** + * 文件或目录名. + */ + public val name: String, + /** + * 唯一识别标识. + */ + public val id: String, + /** + * 标准绝对路径. + */ + public val path: String, + /** + * 文件长度 (大小) bytes, 目录的 [length] 不确定. + */ + 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(group: Group): RemoteFile = + group.filesRoot.resolveById(id) ?: group.filesRoot.resolve(path) + } + + /** + * 获取这个文件或目录**此时**的详细信息. 当文件或目录不存在时返回 `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 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 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 自己上传的文件时需要管理员权限. + */ + public suspend fun renameTo(name: String): Boolean + + /** + * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. + */ + public suspend fun moveTo(target: RemoteFile): Boolean + + /** + * 将这个目录或文件移动到另一个位置. 操作目录或非 Bot 自己上传的文件时需要管理员权限, 无管理员权限时返回 `false`. + */ + public suspend fun moveTo(path: String): Boolean + + /** + * 创建目录. 目录已经存在或无管理员权限时返回 `false`. + */ + public suspend fun mkdir(): Boolean + + /** + * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回 [emptyFlow]. + */ + public suspend fun listFiles(): Flow + + /** + * 获取该目录下所有文件, 返回的 [RemoteFile] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. 当 [RemoteFile] 表示一个文件时返回空迭代器. + * @param lazy 为 `true` 时惰性获取, 为 `false` 时立即获取全部文件列表. + */ + @JavaFriendlyAPI + public suspend fun listFilesIterator(lazy: Boolean): Iterator + + /** + * 得到相应文件消息, 可以发送. 当 [RemoteFile] 表示一个目录或文件不存在时返回 `null`. + */ + public suspend fun toMessage(): FileMessage? + + /////////////////////////////////////////////////////////////////////////// + // upload & download + /////////////////////////////////////////////////////////////////////////// + + /** + * 上传进度回调 + */ + public interface ProgressionCallback { + public fun onBegin(file: RemoteFile, resource: ExternalResource) {} + public fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) {} + public fun onSuccess(file: RemoteFile, resource: ExternalResource) {} + public fun onFailure(file: RemoteFile, resource: ExternalResource, exception: Throwable) {} + + public companion object { + @JvmStatic + @MiraiExperimentalApi + public fun SendChannel.asProgressionCallback(closeOnFinish: Boolean = true): ProgressionCallback { + return object : ProgressionCallback { + override fun onProgression(file: RemoteFile, resource: ExternalResource, downloadedSize: Long) { + offer(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] 传递进度. 当无权上传或其他原因失败时返回 `false`. + * + * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件. + * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend]. + * + * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件. + * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件, + * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件. + * + * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. + */ + public suspend fun upload( + resource: ExternalResource, + callback: ProgressionCallback? = null + ): Boolean + + /** + * 上传文件到 [RemoteFile] 表示的路径. 当无权上传或其他原因失败时返回 `false`. + * + * 上传后不会发送文件消息, 即官方客户端只能在 "群文件" 中查看文件. + * 可通过 [toMessage] 获取到文件消息并通过 [Group.sendMessage] 发送, 或使用 [uploadAndSend]. + * + * 若 [RemoteFile.id] 存在且旧文件存在, 将会覆盖旧文件. + * 即使用 [resolve] 或 [resolveSibling] 获取到的 [RemoteFile] 的 [upload] 总是上传一个新文件, + * 而使用 [resolveById] 或 [listFiles] 获取到的总是覆盖旧文件, 当旧文件已在远程删除时上传一个新文件. + * + * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. + * @see upload + */ + public suspend fun upload(resource: ExternalResource): Boolean = upload(resource, null) + + /** + * 上传文件. + * @see upload + */ + public suspend fun upload(file: File): Boolean = file.toExternalResource().use { upload(it) } + + /** + * 上传文件并发送文件消息. + * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. + * @see upload + */ + @MiraiExperimentalApi + public suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt + + /** + * 上传文件并发送文件消息. + * @see uploadAndSend + */ + @MiraiExperimentalApi + public suspend fun uploadAndSend(file: File): MessageReceipt = + file.toExternalResource().use { uploadAndSend(it) } + + /** + * 获取文件下载链接, 当文件不存在或 [RemoteFile] 表示一个目录时返回 `null` + */ + public suspend fun getDownloadInfo(): DownloadInfo? + + public class DownloadInfo @MiraiInternalApi constructor( + /** + * @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 { + return "DownloadInfo(filename='$filename', path='$path', url='$url', sha1=${sha1.toUHexString("")}, " + + "md5=${md5.toUHexString("")})" + } + } + + public companion object { + /** + * 根目录路径 + * @see RemoteFile.path + */ + public const val ROOT_PATH: String = "/" + + /** + * 上传文件并获取文件消息. + * @param path 远程路径. 起始字符为 '/'. 如 '/foo/bar.txt' + * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. + * @see RemoteFile.upload + */ + @JvmStatic + public suspend fun FileSupported.uploadFile(path: String, resource: ExternalResource): FileMessage { + val file = this.filesRoot.resolve(path) + if (!file.upload(resource)) error("Failed to upload file") + return file.toMessage() ?: error("Failed to create FileMessage.") + } + + /** + * 上传文件并获取文件消息. + * @param resource 需要上传的文件资源. 无论上传是否成功, 本函数都不会关闭 [resource]. + * @see RemoteFile.upload + */ + @JvmStatic + public suspend fun C.uploadFileAndSend( + path: String, + resource: ExternalResource + ): MessageReceipt { + val file = this.filesRoot.resolve(path) + if (!file.upload(resource)) { + error("Failed to upload file") + } + return file.toMessage()?.sendTo(this) ?: error("Failed to create FileMessage.") + } + } +} \ No newline at end of file diff --git a/mirai-core-utils/src/androidMain/kotlin/Actuals.kt b/mirai-core-utils/src/androidMain/kotlin/Actuals.kt index 3a02e12ff..d45e6a784 100644 --- a/mirai-core-utils/src/androidMain/kotlin/Actuals.kt +++ b/mirai-core-utils/src/androidMain/kotlin/Actuals.kt @@ -15,6 +15,10 @@ package net.mamoe.mirai.utils import android.util.Base64 -public actual fun ByteArray.encodeToBase64(): String { +public actual fun ByteArray.encodeBase64(): String { return Base64.encodeToString(this, Base64.DEFAULT) +} + +public actual fun String.decodeBase64(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) } \ No newline at end of file diff --git a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt index 57bf2e0dd..fb881502d 100644 --- a/mirai-core-utils/src/commonMain/kotlin/Bytes.kt +++ b/mirai-core-utils/src/commonMain/kotlin/Bytes.kt @@ -152,7 +152,8 @@ public fun UByteArray.toUHexString(separator: String = " ", offset: Int = 0, len public inline fun ByteArray.encodeToString(offset: Int = 0, charset: Charset = Charsets.UTF_8): String = kotlinx.io.core.String(this, charset = charset, offset = offset, length = this.size - offset) -public expect fun ByteArray.encodeToBase64(): String +public expect fun ByteArray.encodeBase64(): String +public expect fun String.decodeBase64(): ByteArray public inline fun ByteArray.toReadPacket(offset: Int = 0, length: Int = this.size - offset): ByteReadPacket = ByteReadPacket(this, offset = offset, length = length) diff --git a/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt b/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt index c45b7a34c..af2e387c5 100644 --- a/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt +++ b/mirai-core-utils/src/commonMain/kotlin/MiraiPlatformUtils.kt @@ -47,7 +47,11 @@ public fun ByteArray.unzip(offset: Int = 0, length: Int = size - offset): ByteAr } public fun InputStream.md5(): ByteArray { - val digest = MessageDigest.getInstance("md5") + return digest("md5") +} + +public fun InputStream.digest(algorithm: String): ByteArray { + val digest = MessageDigest.getInstance(algorithm) digest.reset() use { input -> object : OutputStream() { @@ -65,6 +69,10 @@ public fun InputStream.md5(): ByteArray { return digest.digest() } +public fun InputStream.sha1(): ByteArray { + return digest("SHA-1") +} + /** * Localhost 解析 */ @@ -80,6 +88,14 @@ public fun ByteArray.md5(offset: Int = 0, length: Int = size - offset): ByteArra return MessageDigest.getInstance("MD5").apply { update(this@md5, offset, length) }.digest() } +public fun String.sha1(): ByteArray = toByteArray().sha1() + +@JvmOverloads +public fun ByteArray.sha1(offset: Int = 0, length: Int = size - offset): ByteArray { + checkOffsetAndLength(offset, length) + return MessageDigest.getInstance("SHA-1").apply { update(this@sha1, offset, length) }.digest() +} + @JvmOverloads public fun ByteArray.ungzip(offset: Int = 0, length: Int = size - offset): ByteArray { return GZIPInputStream(inputStream(offset, length)).use { it.readBytes() } diff --git a/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt b/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt index 6c36115cf..ba4160652 100644 --- a/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt +++ b/mirai-core-utils/src/jvmMain/kotlin/Actuals.kt @@ -15,6 +15,10 @@ package net.mamoe.mirai.utils import java.util.* -public actual fun ByteArray.encodeToBase64(): String { +public actual fun ByteArray.encodeBase64(): String { return Base64.getEncoder().encodeToString(this) +} + +public actual fun String.decodeBase64(): ByteArray { + return Base64.getDecoder().decode(this) } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt index cbeabd14b..ddac6c530 100644 --- a/mirai-core/src/commonMain/kotlin/MiraiImpl.kt +++ b/mirai-core/src/commonMain/kotlin/MiraiImpl.kt @@ -65,6 +65,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor { MessageSerializers.registerSerializer(OnlineGroupImageImpl::class, OnlineGroupImageImpl.serializer()) MessageSerializers.registerSerializer(MarketFaceImpl::class, MarketFaceImpl.serializer()) + MessageSerializers.registerSerializer(FileMessageImpl::class, FileMessageImpl.serializer()) // MessageSource diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt index fe26b64de..0d70412d5 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt @@ -35,6 +35,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache +import net.mamoe.mirai.internal.utils.RemoteFileImpl import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.MessageReceipt import net.mamoe.mirai.message.data.* @@ -72,6 +73,8 @@ internal class GroupImpl( override lateinit var owner: NormalMember override lateinit var botAsMember: NormalMember + override val filesRoot: RemoteFile by lazy { RemoteFileImpl(this, "/") } + override val members: ContactList = ContactList(members.mapNotNullTo(ConcurrentLinkedQueue()) { if (it.uin == bot.id) { botAsMember = newMember(it).cast() diff --git a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt index 6a0fbe68d..196957e0c 100644 --- a/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/contact/SendMessageHandler.kt @@ -10,6 +10,9 @@ package net.mamoe.mirai.internal.contact import contact.StrangerImpl +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import net.mamoe.mirai.contact.* import net.mamoe.mirai.event.nextEventOrNull import net.mamoe.mirai.internal.MiraiImpl @@ -21,6 +24,7 @@ import net.mamoe.mirai.internal.network.Packet import net.mamoe.mirai.internal.network.QQAndroidClient import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket +import net.mamoe.mirai.internal.network.protocol.packet.chat.FileManagement import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.* @@ -120,7 +124,7 @@ internal abstract class SendMessageHandler { val group = contact - var source: OnlineMessageSource.Outgoing? = null + var source: Deferred? = null bot.network.run { sendMessageMultiProtocol( @@ -162,34 +166,37 @@ internal abstract class SendMessageHandler { is MusicSharePacket.Response -> { resp.pkg.checkSuccess("send music share") - source = constructSourceFromMusicShareResponse(finalMessage, resp) + source = CompletableDeferred(constructSourceForSpecialMessage(finalMessage, 3116)) } +// is CommonOidbResponse<*> -> { +// when (resp.toResult("send message").getOrThrow()) { +// is Oidb0x6d9.FeedsRspBody -> { +// } +// } +// } } } - check(source != null) { - "Internal error: source is not initialized" - } + val sourceAwait = source?.await() ?: error("Internal error: source is not initialized") try { - source!!.ensureSequenceIdAvailable() + sourceAwait.ensureSequenceIdAvailable() } catch (e: Exception) { bot.network.logger.warning( "Timeout awaiting sequenceId for message(${finalMessage.content.take(10)}). Some features may not work properly", e - ) } - return MessageReceipt(source!!, contact) + return MessageReceipt(sourceAwait, contact) } } - private fun sendMessageMultiProtocol( + private suspend fun sendMessageMultiProtocol( client: QQAndroidClient, message: MessageChain, fragmented: Boolean, - sourceCallback: (OnlineMessageSource.Outgoing) -> Unit + sourceCallback: (Deferred) -> Unit ): List { message.takeSingleContent()?.let { musicShare -> return listOf( @@ -200,6 +207,12 @@ internal abstract class SendMessageHandler { ) } + message.takeSingleContent()?.let { file -> + file.checkIsImpl() + sourceCallback(contact.async { constructSourceForSpecialMessage(message, 2021) }) + return listOf(FileManagement.Feed(client, contact.id, file.busId, file.id)) + } + return messageSvcSendMessage(client, contact, message, fragmented, sourceCallback) } @@ -208,12 +221,12 @@ internal abstract class SendMessageHandler { contact: C, message: MessageChain, fragmented: Boolean, - sourceCallback: (OnlineMessageSource.Outgoing) -> Unit, + sourceCallback: (Deferred) -> Unit, ) -> List - abstract suspend fun constructSourceFromMusicShareResponse( + abstract suspend fun constructSourceForSpecialMessage( finalMessage: MessageChain, - response: MusicSharePacket.Response + fromAppId: Int, ): OnlineMessageSource.Outgoing open suspend fun uploadLongMessageHighway( @@ -321,39 +334,39 @@ internal sealed class UserSendMessageHandler( ) : SendMessageHandler() { override val senderName: String get() = bot.nick - override suspend fun constructSourceFromMusicShareResponse( + override suspend fun constructSourceForSpecialMessage( finalMessage: MessageChain, - response: MusicSharePacket.Response + fromAppId: Int ): OnlineMessageSource.Outgoing { - throw UnsupportedOperationException("Sending MusicShare to user is not yet supported") + throw UnsupportedOperationException("Sending MusicShare or FileMessage to User is not yet supported") } } internal class FriendSendMessageHandler( contact: FriendImpl, ) : UserSendMessageHandler(contact) { - override val messageSvcSendMessage: (client: QQAndroidClient, contact: FriendImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List = + override val messageSvcSendMessage: (client: QQAndroidClient, contact: FriendImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred) -> Unit) -> List = MessageSvcPbSendMsg::createToFriend } internal class StrangerSendMessageHandler( contact: StrangerImpl, ) : UserSendMessageHandler(contact) { - override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List = + override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred) -> Unit) -> List = MessageSvcPbSendMsg::createToStranger } internal class GroupTempSendMessageHandler( contact: NormalMemberImpl, ) : UserSendMessageHandler(contact) { - override val messageSvcSendMessage: (client: QQAndroidClient, contact: NormalMemberImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List = + override val messageSvcSendMessage: (client: QQAndroidClient, contact: NormalMemberImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred) -> Unit) -> List = MessageSvcPbSendMsg::createToTemp } internal class GroupSendMessageHandler( override val contact: GroupImpl, ) : SendMessageHandler() { - override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List = + override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred) -> Unit) -> List = MessageSvcPbSendMsg::createToGroup override val senderName: String get() = contact.botAsMember.nameCardOrNick @@ -371,14 +384,13 @@ internal class GroupSendMessageHandler( } }.toMessageChain() - - override suspend fun constructSourceFromMusicShareResponse( + override suspend fun constructSourceForSpecialMessage( finalMessage: MessageChain, - response: MusicSharePacket.Response + fromAppId: Int ): OnlineMessageSource.Outgoing { val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt = - nextEventOrNull(3000) { it.fromAppId == 3116 } + nextEventOrNull(3000) { it.fromAppId == fromAppId } ?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY return OnlineMessageSourceToGroupImpl( diff --git a/mirai-core/src/commonMain/kotlin/contact/util.kt b/mirai-core/src/commonMain/kotlin/contact/util.kt index 403bcb9d7..c1481d890 100644 --- a/mirai-core/src/commonMain/kotlin/contact/util.kt +++ b/mirai-core/src/commonMain/kotlin/contact/util.kt @@ -13,138 +13,20 @@ package net.mamoe.mirai.internal.contact import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* -import net.mamoe.mirai.event.broadcast import net.mamoe.mirai.event.events.* -import net.mamoe.mirai.internal.asQQAndroidBot import net.mamoe.mirai.internal.message.LongMessageInternal -import net.mamoe.mirai.internal.message.OnlineMessageSourceToFriendImpl -import net.mamoe.mirai.internal.message.OnlineMessageSourceToStrangerImpl -import net.mamoe.mirai.internal.message.ensureSequenceIdAvailable -import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg -import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToFriend -import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToStranger import net.mamoe.mirai.internal.utils.estimateLength import net.mamoe.mirai.message.* import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.cast import net.mamoe.mirai.utils.castOrNull import net.mamoe.mirai.utils.verbose -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract internal inline val Group.uin: Long get() = this.cast().uin internal inline val Group.groupCode: Long get() = this.id internal inline val User.uin: Long get() = this.id internal inline val Bot.uin: Long get() = this.id -internal suspend fun Friend.sendMessageImpl( - message: Message, - friendReceiptConstructor: (OnlineMessageSourceToFriendImpl) -> MessageReceipt, - tReceiptConstructor: (OnlineMessageSourceToFriendImpl) -> MessageReceipt -): MessageReceipt { - contract { callsInPlace(friendReceiptConstructor, InvocationKind.EXACTLY_ONCE) } - val bot = bot.asQQAndroidBot() - - val chain = kotlin.runCatching { - FriendMessagePreSendEvent(this, message).broadcast() - }.onSuccess { - check(!it.isCancelled) { - throw EventCancelledException("cancelled by FriendMessagePreSendEvent") - } - }.getOrElse { - throw EventCancelledException("exception thrown when broadcasting FriendMessagePreSendEvent", it) - }.message.toMessageChain() - chain.verityLength(message, this) - - chain.firstIsInstanceOrNull()?.source?.ensureSequenceIdAvailable() - - - lateinit var source: OnlineMessageSourceToFriendImpl - val result = bot.network.runCatching { - MessageSvcPbSendMsg.createToFriend( - bot.client, - this@sendMessageImpl, - chain, - false - ) { - source = it - }.forEach { packet -> - packet.sendAndExpect().let { - check(it is MessageSvcPbSendMsg.Response.SUCCESS) { - "Send friend message failed: $it" - } - } - } - friendReceiptConstructor(source) - } - - result.fold( - onSuccess = { - FriendMessagePostSendEvent(this, chain, null, it) - }, - onFailure = { - FriendMessagePostSendEvent(this, chain, it, null) - } - ).broadcast() - - result.getOrThrow() - return tReceiptConstructor(source) -} - -internal suspend fun Stranger.sendMessageImpl( - message: Message, - strangerReceiptConstructor: (OnlineMessageSourceToStrangerImpl) -> MessageReceipt, - tReceiptConstructor: (OnlineMessageSourceToStrangerImpl) -> MessageReceipt -): MessageReceipt { - contract { callsInPlace(strangerReceiptConstructor, InvocationKind.EXACTLY_ONCE) } - val bot = bot.asQQAndroidBot() - - val chain = kotlin.runCatching { - StrangerMessagePreSendEvent(this, message).broadcast() - }.onSuccess { - check(!it.isCancelled) { - throw EventCancelledException("cancelled by StrangerMessagePreSendEvent") - } - }.getOrElse { - throw EventCancelledException("exception thrown when broadcasting StrangerMessagePreSendEvent", it) - }.message.toMessageChain() - chain.verityLength(message, this) - - chain.firstIsInstanceOrNull()?.source?.ensureSequenceIdAvailable() - - lateinit var source: OnlineMessageSourceToStrangerImpl - val result = bot.network.runCatching { - MessageSvcPbSendMsg.createToStranger( - bot.client, - this@sendMessageImpl, - chain, - false, - ) { - source = it - }.forEach { pk -> - pk.sendAndExpect() - .let { - kotlin.check(it is net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg.Response.SUCCESS) { - "Send temp message failed: $it" - } - } - } - strangerReceiptConstructor(source) - } - - result.fold( - onSuccess = { - StrangerMessagePostSendEvent(this, chain, null, it) - }, - onFailure = { - StrangerMessagePostSendEvent(this, chain, it, null) - } - ).broadcast() - - result.getOrThrow() - return tReceiptConstructor(source) -} - internal fun Contact.logMessageSent(message: Message) { if (message !is LongMessageInternal) { bot.logger.verbose("$this <- $message".replaceMagicCodes()) diff --git a/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt b/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt new file mode 100644 index 000000000..8268ad206 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/FileMessageImpl.kt @@ -0,0 +1,55 @@ +/* + * 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/master/LICENSE + */ + + +package net.mamoe.mirai.internal.message + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.mamoe.mirai.message.data.FileMessage +import kotlin.contracts.contract + +internal fun FileMessage.checkIsImpl(): FileMessageImpl { + contract { returns() implies (this@checkIsImpl is FileMessageImpl) } + return this as? FileMessageImpl ?: error("FileMessage must not be implemented manually.") +} + +@Serializable +@SerialName(FileMessage.SERIAL_NAME) +internal class FileMessageImpl( + override val name: String, + override val id: String, + override val size: Long, + val busId: Int // internal // TODO: 2021/3/8 introduce OnlineFileMessage and OfflineFileMessage to eliminate property `busId`. +) : FileMessage { + override fun toString(): String = "[mirai:file:$name,$id]" + + @Suppress("DuplicatedCode") + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FileMessageImpl + + if (name != other.name) return false + if (id != other.id) return false + if (size != other.size) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + id.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + busId.hashCode() + return result + } + +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt index e82f7d333..b35baa9dc 100644 --- a/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt +++ b/mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt @@ -9,19 +9,20 @@ package net.mamoe.mirai.internal.message +import io.ktor.util.* import kotlinx.io.core.discardExact import kotlinx.io.core.readUInt +import kotlinx.io.core.readUShort +import kotlinx.serialization.json.Json import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.internal.asQQAndroidBot import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toVoice -import net.mamoe.mirai.internal.network.protocol.data.proto.CustomFace -import net.mamoe.mirai.internal.network.protocol.data.proto.HummerCommelem -import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody -import net.mamoe.mirai.internal.network.protocol.data.proto.MsgComm +import net.mamoe.mirai.internal.network.protocol.data.proto.* import net.mamoe.mirai.internal.utils.io.serialization.loadAs +import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf import net.mamoe.mirai.message.data.* import net.mamoe.mirai.utils.* @@ -144,10 +145,12 @@ private object ReceiveMessageTransformer { element.lightApp != null -> decodeLightApp(element.lightApp, builder) element.customElem != null -> decodeCustomElem(element.customElem, builder) element.commonElem != null -> decodeCommonElem(element.commonElem, builder) + element.transElemInfo != null -> decodeTransElem(element.transElemInfo, builder) element.elemFlags2 != null || element.extraInfo != null - || element.generalFlags != null -> { + || element.generalFlags != null + -> { // ignore } else -> { @@ -342,6 +345,55 @@ private object ReceiveMessageTransformer { } } + private fun decodeTransElem( + transElement: ImMsgBody.TransElem, + list: MessageChainBuilder + ) { + // file + // type=24 + when (transElement.elemType) { + 24 -> transElement.elemValue.read { + // group file feed + // 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63 +// fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? { +// if (file.ext.isEmpty()) return null +// val element = kotlin.runCatching { +// jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject +// }.getOrNull() ?: return null +// val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null +// return extInfo.loadAs(HummerResv21.ResvAttr.serializer()) +// } + + val var7 = readByte() + if (var7 == 1.toByte()) { + while (remaining > 2) { + val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readUShort().toInt()) + // proto.msgType=6 + + val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only. +// val attr = getFileRsrvAttr(file) ?: continue +// val info = attr.forwardExtFileInfo ?: continue + + list.add( + FileMessageImpl( + name = file.fileName, + id = file.filePath, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a + size = file.fileSize, + busId = file.busId + ) + ) + } + } + } + } + + } + + private val jsonForFileDecode = Json { + isLenient = true + coerceInputValues = true + } + private fun decodeCommonElem( commonElem: ImMsgBody.CommonElem, list: MessageChainBuilder diff --git a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt index fdab19beb..1809c2ec9 100644 --- a/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/incomingSourceImpl.kt @@ -28,7 +28,7 @@ import net.mamoe.mirai.internal.utils.io.serialization.toByteArray import net.mamoe.mirai.message.data.MessageChain import net.mamoe.mirai.message.data.MessageSourceKind import net.mamoe.mirai.message.data.OnlineMessageSource -import net.mamoe.mirai.utils.encodeToBase64 +import net.mamoe.mirai.utils.encodeBase64 import net.mamoe.mirai.utils.encodeToString import net.mamoe.mirai.utils.mapToIntArray import java.util.concurrent.atomic.AtomicBoolean @@ -174,7 +174,7 @@ internal class OnlineMessageSourceFromGroupImpl( ?: error("cannot find member for OnlineMessageSourceFromGroupImpl. msg=${msg._miraiContentToString()}") anonymousInfo.run { - group.newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeToBase64()) + group.newAnonymous(anonGroupMsg!!.anonNick.encodeToString(), anonGroupMsg.anonId.encodeBase64()) } } diff --git a/mirai-core/src/commonMain/kotlin/network/highway/ChunkedFlowSession.kt b/mirai-core/src/commonMain/kotlin/network/highway/ChunkedFlowSession.kt index e8508fc8c..86dff2faf 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/ChunkedFlowSession.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/ChunkedFlowSession.kt @@ -23,7 +23,8 @@ import kotlin.contracts.contract internal class ChunkedFlowSession( private val input: InputStream, private val buffer: ByteArray, - private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T + private val callback: Highway.ProgressionCallback? = null, + private val mapper: (buffer: ByteArray, size: Int, offset: Long) -> T, ) : Closeable { override fun close() { input.close() @@ -38,6 +39,7 @@ internal class ChunkedFlowSession( val size = runBIO { input.read(buffer) } if (size == -1) return block(mapper(buffer, size, offset.getAndAdd(size.toLongUnsigned()))) + callback?.onProgression(offset.get()) } } } diff --git a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt index 8b972759b..7ae154342 100644 --- a/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/highway/Highway.kt @@ -46,6 +46,10 @@ internal object Highway { var extendInfo: ByteArray? = null, ) + fun interface ProgressionCallback { + fun onProgression(size: Long) + } + suspend fun uploadResourceBdh( bot: QQAndroidBot, resource: ExternalResource, @@ -53,10 +57,17 @@ internal object Highway { commandId: Int, // group image=2, friend image=1, groupPtt=29 extendInfo: ByteArray = EMPTY_BYTE_ARRAY, encrypt: Boolean = false, - initialTicket: ByteArray? = null, + initialTicket: ByteArray? = null, // null then use sig session tryOnce: Boolean = false, noBdhAwait: Boolean = false, - fallbackSession: (Throwable) -> BdhSession = { throw IllegalStateException("Failed to get bdh session", it) } + fallbackSession: (Throwable) -> BdhSession = { throw IllegalStateException("Failed to get bdh session", it) }, + resultChecker: (CSDataHighwayHead.RspDataHighwayHead) -> Boolean = { it.errorCode == 0 }, + createConnection: suspend (ip: String, port: Int) -> HighwayProtocolChannel = { ip, port -> + PlatformSocket.connect(ip, port) + }, + callback: ProgressionCallback? = null, + dataFlag: Int = 4096, + localeId: Int = 2052, ): BdhUploadResponse { val bdhSession = kotlin.runCatching { val deferred = bot.bdhSyncer.bdhSession @@ -83,11 +94,15 @@ internal object Highway { commandId = commandId, initialTicket = initialTicket ?: bdhSession.sigSession, data = resource, + dataFlag = dataFlag, + localeId = localeId, fileMd5 = md5, - extendInfo = if (encrypt) TEA.encrypt(extendInfo, bdhSession.sessionKey) else extendInfo + extendInfo = if (encrypt) TEA.encrypt(extendInfo, bdhSession.sessionKey) else extendInfo, + callback = callback ).sendConcurrently( - createConnection = { PlatformSocket.connect(ip, port) }, - coroutines = bot.configuration.highwayUploadCoroutineCount + createConnection = { createConnection(ip, port) }, + coroutines = bot.configuration.highwayUploadCoroutineCount, + resultChecker = resultChecker, ) { head -> if (head.rspExtendinfo.isNotEmpty()) { resp.extendInfo = head.rspExtendinfo @@ -106,6 +121,8 @@ internal enum class ResourceKind( PRIVATE_VOICE("private voice"), GROUP_VOICE("group voice"), + GROUP_FILE("group file"), + LONG_MESSAGE("long message"), FORWARD_MESSAGE("forward message"), ; @@ -123,9 +140,9 @@ internal enum class ChannelKind( override fun toString(): String = display } -internal suspend inline fun tryServersUpload( +internal suspend inline fun tryServersUpload( bot: QQAndroidBot, - servers: Collection>, + servers: Collection>, resourceSize: Long, resourceKind: ResourceKind, channelKind: ChannelKind, @@ -250,10 +267,51 @@ private fun Flow.produceIn0(coroutineScope: CoroutineScope): ReceiveChann } } +internal interface HighwayProtocolChannel { + suspend fun send(packet: ByteReadPacket) + suspend fun read(): ByteReadPacket +} + + +// backup + +// createConnection = { ip, port -> +// SynchronousHighwayProtocolChannel { packet -> +// val http = Mirai.Http +// http.post("http://$ip:$port/cgi-bin/httpconn?htcmd=0x6FF0087&uin=${bot.id}") { +// userAgent("QQClient") +// val bytes = packet.readBytes() +// body = object : OutgoingContent.WriteChannelContent() { +// override val contentLength: Long get() = bytes.size.toLongUnsigned() +// override val contentType: ContentType get() = ContentType.Any +// override suspend fun writeTo(channel: ByteWriteChannel) { +// channel.writeFully(bytes) +// } +// } +// } +// } +// } + +internal class SynchronousHighwayProtocolChannel( + val action: suspend (ByteReadPacket) -> ByteArray +) : HighwayProtocolChannel { + @Volatile + var result: ByteArray? = null + + override suspend fun send(packet: ByteReadPacket) { + result = action(packet) + } + + override suspend fun read(): ByteReadPacket { + return result?.toReadPacket() ?: error("result is null") + } +} + internal suspend fun ChunkedFlowSession.sendConcurrently( - createConnection: suspend () -> PlatformSocket, + createConnection: suspend () -> HighwayProtocolChannel, coroutines: Int = 5, - respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {} + resultChecker: (CSDataHighwayHead.RspDataHighwayHead) -> Boolean, + respCallback: (resp: CSDataHighwayHead.RspDataHighwayHead) -> Unit = {}, ) = coroutineScope { val channel = asFlow().produceIn0(this) // 'single thread' producer emits chunks to channel @@ -264,7 +322,7 @@ internal suspend fun ChunkedFlowSession.sendConcurrently( while (isActive) { val next = channel.tryReceive() ?: break // concurrent-safe receive val result = next.withUse { - socket.sendReceiveHighway(next) + socket.sendReceiveHighway(next, resultChecker) } respCallback(result) } @@ -282,8 +340,9 @@ private suspend fun ReceiveChannel.tryReceive(): E? { }.getOrNull() } -private suspend fun PlatformSocket.sendReceiveHighway( +private suspend fun HighwayProtocolChannel.sendReceiveHighway( it: ByteReadPacket, + resultChecker: (CSDataHighwayHead.RspDataHighwayHead) -> Boolean, ): CSDataHighwayHead.RspDataHighwayHead { send(it) //0A 3C 08 01 12 0A 31 39 39 34 37 30 31 30 32 31 1A 0C 50 69 63 55 70 2E 44 61 74 61 55 70 20 E9 A7 05 28 00 30 BD DB 8B 80 02 38 80 20 40 02 4A 0A 38 2E 32 2E 30 2E 31 32 39 36 50 84 10 12 3D 08 00 10 FD 08 18 00 20 FD 08 28 C6 01 38 00 42 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 4A 10 D4 1D 8C D9 8F 00 B2 04 E9 80 09 98 EC F8 42 7E 50 89 92 A2 FB 06 58 00 60 00 18 53 20 01 28 00 30 04 3A 00 40 E6 B7 F7 D9 80 2E 48 00 50 00 @@ -293,7 +352,9 @@ private suspend fun PlatformSocket.sendReceiveHighway( val headLength = readInt() discardExact(4) val proto = readProtoBuf(CSDataHighwayHead.RspDataHighwayHead.serializer(), length = headLength) - check(proto.errorCode == 0) { "highway transfer failed, error ${proto.errorCode}" } + check(resultChecker(proto)) { "highway transfer failed, error ${proto.errorCode}" } + // error 70: 某属性有误 + // error 79: 没有 body (可能) return proto } } @@ -305,19 +366,20 @@ internal fun highwayPacketSession( appId: Int, dataFlag: Int = 4096, commandId: Int, - localId: Int = 2052, + localeId: Int = 2052, initialTicket: ByteArray, data: ExternalResource, fileMd5: ByteArray, sizePerPacket: Int = ByteArrayPool.BUFFER_SIZE, extendInfo: ByteArray = EMPTY_BYTE_ARRAY, + callback: Highway.ProgressionCallback? = null, ): ChunkedFlowSession { ByteArrayPool.checkBufferSize(sizePerPacket) // require(ticket.size == 128) { "bad uKey. Required size=128, got ${ticket.size}" } val ticket = AtomicReference(initialTicket) - return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket)) { buffer, size, offset -> + return ChunkedFlowSession(data.inputStream(), ByteArray(sizePerPacket), callback) { buffer, size, offset -> val head = CSDataHighwayHead.ReqDataHighwayHead( msgBasehead = CSDataHighwayHead.DataHighwayHead( version = 1, @@ -328,13 +390,13 @@ internal fun highwayPacketSession( 1 -> client.nextHighwayDataTransSequenceIdForFriend() 27 -> client.nextHighwayDataTransSequenceIdForApplyUp() 29 -> client.nextHighwayDataTransSequenceIdForGroup() - else -> error("illegal commandId: $commandId") + else -> client.nextHighwayDataTransSequenceIdForGroup() }, retryTimes = 0, appid = appId, dataflag = dataFlag, commandId = commandId, - localeId = localId + localeId = localeId ), msgSeghead = CSDataHighwayHead.SegHead( // cacheAddr = 812157193, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Exciting.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Exciting.kt new file mode 100644 index 000000000..001c4cbb5 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Exciting.kt @@ -0,0 +1,81 @@ +/* + * 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/master/LICENSE + */ + + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +@Serializable +internal class GroupFileUploadExt( + @JvmField @ProtoNumber(1) val u1: Int, + @JvmField @ProtoNumber(2) val u2: Int, + @JvmField @ProtoNumber(100) val entry: GroupFileUploadEntry, + @JvmField @ProtoNumber(3) val u3: Int, +) : ProtoBuf + +@Serializable +internal class GroupFileUploadEntry( + @JvmField @ProtoNumber(100) val business: ExcitingBusiInfo, + @JvmField @ProtoNumber(200) val fileEntry: ExcitingFileEntry, + @JvmField @ProtoNumber(300) val clientInfo: ExcitingClientInfo, + @JvmField @ProtoNumber(400) val fileNameInfo: ExcitingFileNameInfo, + @JvmField @ProtoNumber(500) val host: ExcitingHostConfig, +) : ProtoBuf + +@Serializable +internal class ExcitingBusiInfo( + @JvmField @ProtoNumber(1) val busId: Int, + @JvmField @ProtoNumber(100) val senderUin: Long, + @JvmField @ProtoNumber(200) val receiverUin: Long, // maybe + @JvmField @ProtoNumber(400) val groupCode: Long, // maybe +) : ProtoBuf + +@Serializable +internal class ExcitingFileEntry( + @JvmField @ProtoNumber(100) val fileSize: Long, + @JvmField @ProtoNumber(200) val md5: ByteArray, + @JvmField @ProtoNumber(300) val sha1: ByteArray, + @JvmField @ProtoNumber(600) val fileId: ByteArray, + @JvmField @ProtoNumber(700) val uploadKey: ByteArray, +) : ProtoBuf + + +@Serializable +internal class ExcitingClientInfo( + @JvmField @ProtoNumber(100) val clientType: Int, // maybe + @JvmField @ProtoNumber(200) val appId: String, + @JvmField @ProtoNumber(300) val terminalType: Int, + @JvmField @ProtoNumber(400) val clientVer: String, + @JvmField @ProtoNumber(600) val unknown: Int, +) : ProtoBuf + +@Serializable +internal class ExcitingFileNameInfo( + @JvmField @ProtoNumber(100) val filename: String, +) : ProtoBuf + +@Serializable +internal class ExcitingHostConfig( + @JvmField @ProtoNumber(200) val hosts: List, +) : ProtoBuf + +@Serializable +internal class ExcitingHostInfo( + @JvmField @ProtoNumber(1) val url: ExcitingUrlInfo, + @JvmField @ProtoNumber(2) val port: Int, +) : ProtoBuf + +@Serializable +internal class ExcitingUrlInfo( + @JvmField @ProtoNumber(1) val unknown: Int, + @JvmField @ProtoNumber(2) val host: String, +) : ProtoBuf \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/GroupFileCommon.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/GroupFileCommon.kt new file mode 100644 index 000000000..0578c2712 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/GroupFileCommon.kt @@ -0,0 +1,104 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused", "SpellCheckingInspection") + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +internal class GroupFileCommon : ProtoBuf { + @Serializable + internal class FeedsInfo( + @JvmField @ProtoNumber(1) val busId: Int = 0, + @JvmField @ProtoNumber(2) val fileId: String = "", + @JvmField @ProtoNumber(3) val msgRandom: Int = 0, + @JvmField @ProtoNumber(4) val ext: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(5) val feedFlag: Int = 0, + @JvmField @ProtoNumber(6) val msgCtrl: MsgCtrl.MsgCtrl? = null + ) : ProtoBuf + + @Serializable + internal class FeedsResult( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val detail: String = "", + @JvmField @ProtoNumber(3) val fileId: String = "", + @JvmField @ProtoNumber(4) val busId: Int = 0, + @JvmField @ProtoNumber(5) val deadTime: Int = 0 + ) : ProtoBuf + + @Serializable + internal class FileInfo( + @JvmField @ProtoNumber(1) val fileId: String = "", + @JvmField @ProtoNumber(2) val fileName: String = "", + @JvmField @ProtoNumber(3) val fileSize: Long = 0L, + @JvmField @ProtoNumber(4) val busId: Int = 0, + @JvmField @ProtoNumber(5) val uploadedSize: Long = 0L, + @JvmField @ProtoNumber(6) val uploadTime: Int = 0, + @JvmField @ProtoNumber(7) val deadTime: Int = 0, + @JvmField @ProtoNumber(8) val modifyTime: Int = 0, + @JvmField @ProtoNumber(9) val downloadTimes: Int = 0, + @JvmField @ProtoNumber(10) val sha: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(11) val sha3: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(12) val md5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(13) val localPath: String = "", + @JvmField @ProtoNumber(14) val uploaderName: String = "", + @JvmField @ProtoNumber(15) val uploaderUin: Long = 0L, + @JvmField @ProtoNumber(16) val parentFolderId: String = "", + @JvmField @ProtoNumber(17) val safeType: Int = 0, + @JvmField @ProtoNumber(20) val fileBlobExt: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(21) val ownerUin: Long = 0L, + @JvmField @ProtoNumber(22) val feedId: String = "", + @JvmField @ProtoNumber(23) val reservedField: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + internal class FileInfoTmem( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val files: List = emptyList() + ) : ProtoBuf + + @Serializable + internal class FileItem( + @JvmField @ProtoNumber(1) val type: Int = 0, + @JvmField @ProtoNumber(2) val folderInfo: FolderInfo? = null, + @JvmField @ProtoNumber(3) val fileInfo: FileInfo? = null + ) : ProtoBuf + + @Serializable + internal class FolderInfo( + @JvmField @ProtoNumber(1) val folderId: String = "", // uuid + @JvmField @ProtoNumber(2) val parentFolderId: String = "", + @JvmField @ProtoNumber(3) val folderName: String = "", + @JvmField @ProtoNumber(4) val createTime: Int = 0, + @JvmField @ProtoNumber(5) val modifyTime: Int = 0, + @JvmField @ProtoNumber(6) val createUin: Long = 0L, + @JvmField @ProtoNumber(7) val creatorName: String = "", + @JvmField @ProtoNumber(8) val totalFileCount: Int = 0, + @JvmField @ProtoNumber(9) val modifyUin: Long = 0L, + @JvmField @ProtoNumber(10) val modifyName: String = "", + @JvmField @ProtoNumber(11) val usedSpace: Long = 0L + ) : ProtoBuf + + @Serializable + internal class FolderInfoTmem( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val folders: List = emptyList() + ) : ProtoBuf + + @Serializable + internal class OverwriteInfo( + @JvmField @ProtoNumber(1) val fileId: String = "", + @JvmField @ProtoNumber(2) val downloadTimes: Int = 0 + ) : ProtoBuf +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Highway.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Highway.kt index 48162f3c9..41020e677 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Highway.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Highway.kt @@ -160,10 +160,10 @@ internal class CSDataHighwayHead : ProtoBuf { @JvmField @ProtoNumber(2) val uin: String = "", @JvmField @ProtoNumber(3) val command: String = "", @JvmField @ProtoNumber(4) val seq: Int = 0, - @JvmField @ProtoNumber(5) val retryTimes: Int,// = 0, - @JvmField @ProtoNumber(6) val appid: Int,// = 0, - @JvmField @ProtoNumber(7) val dataflag: Int,// = 0, - @JvmField @ProtoNumber(8) val commandId: Int,// = 0, + @JvmField @ProtoNumber(5) val retryTimes: Int? = null,// = 0, + @JvmField @ProtoNumber(6) val appid: Int? = null,// = 0, + @JvmField @ProtoNumber(7) val dataflag: Int? = null,// = 0, + @JvmField @ProtoNumber(8) val commandId: Int? = null,// = 0, @JvmField @ProtoNumber(9) val buildVer: String = "", @JvmField @ProtoNumber(10) val localeId: Int = 0, @JvmField @ProtoNumber(11) val envId: Int = 0 @@ -279,9 +279,9 @@ internal class CSDataHighwayHead : ProtoBuf { @JvmField @ProtoNumber(2) val filesize: Long = 0L, @JvmField @ProtoNumber(3) val dataoffset: Long = 0L, @JvmField @ProtoNumber(4) val datalength: Int = 0, - @JvmField @ProtoNumber(5) val rtcode: Int, // = 0, + @JvmField @ProtoNumber(5) val rtcode: Int? = null, // = 0, @JvmField @ProtoNumber(6) val serviceticket: ByteArray = EMPTY_BYTE_ARRAY, - @JvmField @ProtoNumber(7) val flag: Int, // = 0, + @JvmField @ProtoNumber(7) val flag: Int? = null, // = 0, @JvmField @ProtoNumber(8) val md5: ByteArray = EMPTY_BYTE_ARRAY, @JvmField @ProtoNumber(9) val fileMd5: ByteArray = EMPTY_BYTE_ARRAY, @JvmField @ProtoNumber(10) val cacheAddr: Int = 0, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/HummerResv21.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/HummerResv21.kt index 9559200c4..100c6a8d0 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/HummerResv21.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/HummerResv21.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 Mamoe Technologies and contributors. + * 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. @@ -16,7 +16,6 @@ import kotlinx.serialization.protobuf.ProtoNumber import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY import net.mamoe.mirai.internal.utils.io.ProtoBuf -@Serializable internal class HummerResv21 : ProtoBuf { @Serializable internal class FileImgInfo( @@ -29,7 +28,7 @@ internal class HummerResv21 : ProtoBuf { @JvmField @ProtoNumber(1) val fileType: Int = 0, @JvmField @ProtoNumber(2) val senderUin: Long = 0L, @JvmField @ProtoNumber(3) val receiverUin: Long = 0L, - @JvmField @ProtoNumber(4) val fileUuid: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(4) val fileUuid: String = "", @JvmField @ProtoNumber(5) val fileName: String = "", @JvmField @ProtoNumber(6) val fileSize: Long = 0L, @JvmField @ProtoNumber(7) val fileSha1: ByteArray = EMPTY_BYTE_ARRAY, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt index a8f943e0f..ec9914d9f 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt @@ -1192,12 +1192,12 @@ internal class ObjMsg : ProtoBuf { @Serializable internal class MsgFile( @ProtoNumber(1) @JvmField val busId: Int = 0, - @ProtoNumber(2) @JvmField val filePath: ByteArray = EMPTY_BYTE_ARRAY, + @ProtoNumber(2) @JvmField val filePath: String = "", // actually uuid @ProtoNumber(3) @JvmField val fileSize: Long = 0L, @ProtoNumber(4) @JvmField val fileName: String = "", @ProtoNumber(5) @JvmField val int64DeadTime: Long = 0L, - @ProtoNumber(6) @JvmField val fileSha1: ByteArray = EMPTY_BYTE_ARRAY, - @ProtoNumber(7) @JvmField val ext: ByteArray = EMPTY_BYTE_ARRAY + @ProtoNumber(6) @JvmField val fileSha1: ByteArray = EMPTY_BYTE_ARRAY, // empty + @ProtoNumber(7) @JvmField val ext: String = "", // originally bytes ) : ProtoBuf } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt index 34331649c..7992c4bda 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt @@ -958,7 +958,7 @@ internal class OidbSso : ProtoBuf { @Serializable internal class OIDBSSOPkg( @ProtoNumber(1) @JvmField val command: Int = 0, - @ProtoNumber(2) @JvmField val serviceType: Int = 0, + @ProtoNumber(2) @JvmField val serviceType: Int, @ProtoNumber(3) @JvmField val result: Int = 0, @ProtoNumber(4) @JvmField val bodybuffer: ByteArray = EMPTY_BYTE_ARRAY, @ProtoNumber(5) @JvmField val errorMsg: String = "", diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d6.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d6.kt new file mode 100644 index 000000000..d6d9309d5 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d6.kt @@ -0,0 +1,179 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused", "SpellCheckingInspection") + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.network.protocol.packet.chat.CheckableStruct +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +internal class Oidb0x6d6 : ProtoBuf { + @Serializable + internal class DeleteFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val parentFolderId: String = "", + @JvmField @ProtoNumber(5) val fileId: String = "", + @JvmField @ProtoNumber(6) val msgdbSeq: Int = 0, + @JvmField @ProtoNumber(7) val msgRand: Int = 0 + ) : ProtoBuf + + @Serializable + internal class DeleteFileRspBody( + /** + * -103: file not exist + */ + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "" + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class DownloadFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "", + @JvmField @ProtoNumber(5) val boolThumbnailReq: Boolean = false, + @JvmField @ProtoNumber(6) val urlType: Int = 0, + @JvmField @ProtoNumber(7) val boolPreviewReq: Boolean = false, + @JvmField @ProtoNumber(8) val src: Int = 0 + ) : ProtoBuf + + @Serializable + internal class DownloadFileRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val downloadIp: String = "", + @JvmField @ProtoNumber(5) val downloadDns: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(6) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(7) val sha: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(8) val sha3: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(9) val md5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(10) val cookieVal: String = "", + @JvmField @ProtoNumber(11) val saveFileName: String = "", + @JvmField @ProtoNumber(12) val previewPort: Int = 0, + @JvmField @ProtoNumber(13) val downloadDnsHttps: String = "", + @JvmField @ProtoNumber(14) val previewPortHttps: Int = 0 + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class MoveFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "", + @JvmField @ProtoNumber(5) val parentFolderId: String = "", + @JvmField @ProtoNumber(6) val destFolderId: String = "" + ) : ProtoBuf + + @Serializable + internal class MoveFileRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val parentFolderId: String = "" + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class RenameFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "", + @JvmField @ProtoNumber(5) val parentFolderId: String = "", + @JvmField @ProtoNumber(6) val newFileName: String = "" + ) : ProtoBuf + + @Serializable + internal class RenameFileRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "" + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class ReqBody( + @JvmField @ProtoNumber(1) val uploadFileReq: UploadFileReqBody? = null, + @JvmField @ProtoNumber(2) val resendFileReq: ResendReqBody? = null, + @JvmField @ProtoNumber(3) val downloadFileReq: DownloadFileReqBody? = null, + @JvmField @ProtoNumber(4) val deleteFileReq: DeleteFileReqBody? = null, + @JvmField @ProtoNumber(5) val renameFileReq: RenameFileReqBody? = null, + @JvmField @ProtoNumber(6) val moveFileReq: MoveFileReqBody? = null + ) : ProtoBuf + + @Serializable + internal class ResendReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "", + @JvmField @ProtoNumber(5) val sha: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + internal class ResendRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val uploadIp: String = "", + @JvmField @ProtoNumber(5) val fileKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(6) val checkKey: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class RspBody( + @JvmField @ProtoNumber(1) val uploadFileRsp: UploadFileRspBody? = null, + @JvmField @ProtoNumber(2) val resendFileRsp: ResendRspBody? = null, + @JvmField @ProtoNumber(3) val downloadFileRsp: DownloadFileRspBody? = null, + @JvmField @ProtoNumber(4) val deleteFileRsp: DeleteFileRspBody? = null, + @JvmField @ProtoNumber(5) val renameFileRsp: RenameFileRspBody? = null, + @JvmField @ProtoNumber(6) val moveFileRsp: MoveFileRspBody? = null + ) : ProtoBuf + + @Serializable + internal class UploadFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val entrance: Int = 0, + @JvmField @ProtoNumber(5) val parentFolderId: String = "", + @JvmField @ProtoNumber(6) val fileName: String = "", + @JvmField @ProtoNumber(7) val localPath: String = "", + @JvmField @ProtoNumber(8) val fileSize: Long = 0L, + @JvmField @ProtoNumber(9) val sha: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(10) val sha3: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(11) val md5: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(15) val boolSupportMultiUpload: Boolean = false + ) : ProtoBuf + + @Serializable + internal class UploadFileRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val uploadIp: String = "", + @JvmField @ProtoNumber(5) val serverDns: String = "", + @JvmField @ProtoNumber(6) val busId: Int = 0, + @JvmField @ProtoNumber(7) val fileId: String = "", + @JvmField @ProtoNumber(8) val fileKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(9) val checkKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(10) val boolFileExist: Boolean = false, + @JvmField @ProtoNumber(12) val uploadIpLanV4: List = emptyList(), + @JvmField @ProtoNumber(13) val uploadIpLanV6: List = emptyList(), + @JvmField @ProtoNumber(14) val uploadPort: Int = 0 + ) : ProtoBuf, CheckableStruct +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d7.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d7.kt new file mode 100644 index 000000000..e0cad8495 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d7.kt @@ -0,0 +1,99 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused", "SpellCheckingInspection") + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.packet.chat.CheckableStruct +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +internal class Oidb0x6d7 : ProtoBuf { + @Serializable + internal class CreateFolderReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val parentFolderId: String = "", + @JvmField @ProtoNumber(4) val folderName: String = "" + ) : ProtoBuf + + @Serializable + internal class CreateFolderRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val folderInfo: GroupFileCommon.FolderInfo? = null + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class DeleteFolderReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val folderId: String = "" + ) : ProtoBuf + + @Serializable + internal class DeleteFolderRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "" + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class MoveFolderReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val folderId: String = "", + @JvmField @ProtoNumber(4) val parentFolderId: String = "", + @JvmField @ProtoNumber(5) val destFolderId: String = "" + ) : ProtoBuf + + @Serializable + internal class MoveFolderRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val folderInfo: GroupFileCommon.FolderInfo? = null + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class RenameFolderReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val folderId: String = "", + @JvmField @ProtoNumber(4) val newFolderName: String = "" + ) : ProtoBuf + + @Serializable + internal class RenameFolderRspBody( + @ProtoNumber(1) override val int32RetCode: Int = 0, + @ProtoNumber(2) override val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val folderInfo: GroupFileCommon.FolderInfo? = null + ) : ProtoBuf, CheckableStruct + + @Serializable + internal class ReqBody( + @JvmField @ProtoNumber(1) val createFolderReq: CreateFolderReqBody? = null, + @JvmField @ProtoNumber(2) val deleteFolderReq: DeleteFolderReqBody? = null, + @JvmField @ProtoNumber(3) val renameFolderReq: RenameFolderReqBody? = null, + @JvmField @ProtoNumber(4) val moveFolderReq: MoveFolderReqBody? = null + ) : ProtoBuf + + @Serializable + internal class RspBody( + @JvmField @ProtoNumber(1) val createFolderRsp: CreateFolderRspBody? = null, + @JvmField @ProtoNumber(2) val deleteFolderRsp: DeleteFolderRspBody? = null, + @JvmField @ProtoNumber(3) val renameFolderRsp: RenameFolderRspBody? = null, + @JvmField @ProtoNumber(4) val moveFolderRsp: MoveFolderRspBody? = null + ) : ProtoBuf +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d8.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d8.kt new file mode 100644 index 000000000..ecbd996de --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d8.kt @@ -0,0 +1,168 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused", "SpellCheckingInspection") + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +internal class Oidb0x6d8 : ProtoBuf { + @Serializable + internal class FileTimeStamp( + @JvmField @ProtoNumber(1) val uploadTime: Int = 0, + @JvmField @ProtoNumber(2) val fileId: String = "" + ) : ProtoBuf + + @Serializable + internal class GetFileCountReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0 + ) : ProtoBuf + + @Serializable + internal class GetFileCountRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val allFileCount: Int = 0, + @JvmField @ProtoNumber(5) val boolFileTooMany: Boolean = false, + @JvmField @ProtoNumber(6) val limitCount: Int = 0, + @JvmField @ProtoNumber(7) val boolIsFull: Boolean = false + ) : ProtoBuf + + @Serializable + internal class GetFileInfoReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "", + @JvmField @ProtoNumber(5) val fieldFlag: Int = 16777215 + ) : ProtoBuf + + @Serializable + internal class GetFileInfoRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val fileInfo: GroupFileCommon.FileInfo? = null + ) : ProtoBuf + + @Serializable + internal class GetFileListReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val folderId: String = "", + @JvmField @ProtoNumber(4) val startTimestamp: FileTimeStamp? = null, + @JvmField @ProtoNumber(5) val fileCount: Int = 0, + @JvmField @ProtoNumber(6) val maxTimestamp: FileTimeStamp? = null, + @JvmField @ProtoNumber(7) val allFileCount: Int = 0, + @JvmField @ProtoNumber(8) val reqFrom: Int = 0, + @JvmField @ProtoNumber(9) val sortBy: Int = 0, + @JvmField @ProtoNumber(10) val filterCode: Int = 0, + @JvmField @ProtoNumber(11) val uin: Long = 0L, + @JvmField @ProtoNumber(12) val fieldFlag: Int = 16777215, + @JvmField @ProtoNumber(13) val startIndex: Int = 0, + @JvmField @ProtoNumber(14) val context: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(15) val clientVersion: Int = 0, + @JvmField @ProtoNumber(16) val whiteList: Int = 0, + @JvmField @ProtoNumber(17) val sortOrder: Int = 0, + @JvmField @ProtoNumber(18) val showOnlinedocFolder: Int = 0 + ) : ProtoBuf + + @Serializable + internal class GetFileListRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val boolIsEnd: Boolean = false, + @JvmField @ProtoNumber(5) val itemList: List = emptyList(), + @JvmField @ProtoNumber(6) val msgMaxTimestamp: FileTimeStamp? = null, + @JvmField @ProtoNumber(7) val allFileCount: Int = 0, + @JvmField @ProtoNumber(8) val filterCode: Int = 0, + @JvmField @ProtoNumber(11) val boolSafeCheckFlag: Boolean = false, + @JvmField @ProtoNumber(12) val safeCheckRes: Int = 0, + @JvmField @ProtoNumber(13) val nextIndex: Int = 0, + @JvmField @ProtoNumber(14) val context: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(15) val role: Int = 0, + @JvmField @ProtoNumber(16) val openFlag: Int = 0 + ) : ProtoBuf { + @Serializable + internal class Item( + @JvmField @ProtoNumber(1) val type: Int = 0, // folder=2, + @JvmField @ProtoNumber(2) val folderInfo: GroupFileCommon.FolderInfo? = null, + @JvmField @ProtoNumber(3) val fileInfo: GroupFileCommon.FileInfo? = null + ) : ProtoBuf { + val id get() = fileInfo?.fileId ?: folderInfo?.folderId + val name get() = fileInfo?.fileName ?: folderInfo?.folderName + } + } + + @Serializable + internal class GetFilePreviewReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "" + ) : ProtoBuf + + @Serializable + internal class GetFilePreviewRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val int32ServerIp: Int = 0, + @JvmField @ProtoNumber(5) val int32ServerPort: Int = 0, + @JvmField @ProtoNumber(6) val downloadDns: String = "", + @JvmField @ProtoNumber(7) val downloadUrl: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(8) val cookieVal: String = "", + @JvmField @ProtoNumber(9) val reservedField: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(10) val downloadDnsHttps: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(11) val previewPortHttps: Int = 0 + ) : ProtoBuf + + @Serializable + internal class GetSpaceReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0 + ) : ProtoBuf + + @Serializable + internal class GetSpaceRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val totalSpace: Long = 0L, + @JvmField @ProtoNumber(5) val usedSpace: Long = 0L, + @JvmField @ProtoNumber(6) val boolAllUpload: Boolean = false + ) : ProtoBuf + + @Serializable + internal class ReqBody( + @JvmField @ProtoNumber(1) val fileInfoReq: GetFileInfoReqBody? = null, + @JvmField @ProtoNumber(2) val fileListInfoReq: GetFileListReqBody? = null, + @JvmField @ProtoNumber(3) val groupFileCntReq: GetFileCountReqBody? = null, + @JvmField @ProtoNumber(4) val groupSpaceReq: GetSpaceReqBody? = null, + @JvmField @ProtoNumber(5) val filePreviewReq: GetFilePreviewReqBody? = null + ) : ProtoBuf + + @Serializable + internal class RspBody( + @JvmField @ProtoNumber(1) val fileInfoRsp: GetFileInfoRspBody? = null, + @JvmField @ProtoNumber(2) val fileListInfoRsp: GetFileListRspBody? = null, + @JvmField @ProtoNumber(3) val groupFileCntRsp: GetFileCountRspBody? = null, + @JvmField @ProtoNumber(4) val groupSpaceRsp: GetSpaceRspBody? = null, + @JvmField @ProtoNumber(5) val filePreviewRsp: GetFilePreviewRspBody? = null + ) : ProtoBuf +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d9.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d9.kt new file mode 100644 index 000000000..283a6e441 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Oidb0x6d9.kt @@ -0,0 +1,120 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("unused", "SpellCheckingInspection") + +package net.mamoe.mirai.internal.network.protocol.data.proto + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.utils.io.ProtoBuf + +internal class Oidb0x6d9 : ProtoBuf { + @Serializable + internal class CopyFromReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val srcBusId: Int = 0, + @JvmField @ProtoNumber(4) val srcParentFolder: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(5) val srcFilePath: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(6) val dstBusId: Int = 0, + @JvmField @ProtoNumber(7) val dstFolderId: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(8) val fileSize: Long = 0L, + @JvmField @ProtoNumber(9) val localPath: String = "", + @JvmField @ProtoNumber(10) val fileName: String = "", + @JvmField @ProtoNumber(11) val srcUin: Long = 0L, + @JvmField @ProtoNumber(12) val md5: ByteArray = EMPTY_BYTE_ARRAY + ) : ProtoBuf + + @Serializable + internal class CopyFromRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val saveFilePath: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(5) val busId: Int = 0 + ) : ProtoBuf + + @Serializable + internal class CopyToReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val srcBusId: Int = 0, + @JvmField @ProtoNumber(4) val srcFileId: String = "", + @JvmField @ProtoNumber(5) val dstBusId: Int = 0, + @JvmField @ProtoNumber(6) val dstUin: Long = 0L, + @JvmField @ProtoNumber(40) val newFileName: String = "", + @JvmField @ProtoNumber(100) val timCloudPdirKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(101) val timCloudPpdirKey: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(102) val timCloudExtensionInfo: ByteArray = EMPTY_BYTE_ARRAY, + @JvmField @ProtoNumber(103) val timFileExistOption: Int = 0 + ) : ProtoBuf + + @Serializable + internal class CopyToRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val saveFilePath: String = "", + @JvmField @ProtoNumber(5) val busId: Int = 0, + @JvmField @ProtoNumber(40) val fileName: String = "" + ) : ProtoBuf + + @Serializable + internal class FeedsReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val feedsInfoList: List = emptyList(), + @JvmField @ProtoNumber(4) val multiSendSeq: Int = 0 + ) : ProtoBuf + + @Serializable + internal class FeedsRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val feedsResultList: List = emptyList(), + @JvmField @ProtoNumber(5) val svrbusyWaitTime: Int = 0 + ) : ProtoBuf + + @Serializable + internal class ReqBody( + @JvmField @ProtoNumber(1) val transFileReq: TransFileReqBody? = null, + @JvmField @ProtoNumber(2) val copyFromReq: CopyFromReqBody? = null, + @JvmField @ProtoNumber(3) val copyToReq: CopyToReqBody? = null, + @JvmField @ProtoNumber(5) val feedsInfoReq: FeedsReqBody? = null + ) : ProtoBuf + + @Serializable + internal class RspBody( + @JvmField @ProtoNumber(1) val transFileRsp: TransFileRspBody? = null, + @JvmField @ProtoNumber(2) val copyFromRsp: CopyFromRspBody? = null, + @JvmField @ProtoNumber(3) val copyToRsp: CopyToRspBody? = null, + @JvmField @ProtoNumber(5) val feedsInfoRsp: FeedsRspBody? = null + ) : ProtoBuf + + @Serializable + internal class TransFileReqBody( + @JvmField @ProtoNumber(1) val groupCode: Long = 0L, + @JvmField @ProtoNumber(2) val appId: Int = 0, + @JvmField @ProtoNumber(3) val busId: Int = 0, + @JvmField @ProtoNumber(4) val fileId: String = "" + ) : ProtoBuf + + @Serializable + internal class TransFileRspBody( + @JvmField @ProtoNumber(1) val int32RetCode: Int = 0, + @JvmField @ProtoNumber(2) val retMsg: String = "", + @JvmField @ProtoNumber(3) val clientWording: String = "", + @JvmField @ProtoNumber(4) val saveBusId: Int = 0, + @JvmField @ProtoNumber(5) val saveFilePath: String = "" + ) : ProtoBuf +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index ca5f26dab..ad328e122 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -161,6 +161,7 @@ internal object KnownPacketFactories { StrangerList.DelStranger, SummaryCard.ReqSummaryCard, MusicSharePacket, + *FileManagement.factories ) object IncomingFactories : List> by mutableListOf( 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 new file mode 100644 index 000000000..1a0bbbbbb --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/GroupFile.kt @@ -0,0 +1,502 @@ +/* + * 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/master/LICENSE + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package net.mamoe.mirai.internal.network.protocol.packet.chat + +import kotlinx.io.core.ByteReadPacket +import kotlinx.serialization.DeserializationStrategy +import net.mamoe.mirai.internal.QQAndroidBot +import net.mamoe.mirai.internal.network.Packet +import net.mamoe.mirai.internal.network.QQAndroidClient +import net.mamoe.mirai.internal.network.protocol.data.proto.* +import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory +import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket +import net.mamoe.mirai.internal.utils.io.ProtoBuf +import net.mamoe.mirai.internal.utils.io.serialization.readOidbSsoPkg +import net.mamoe.mirai.internal.utils.io.serialization.writeOidb +import net.mamoe.mirai.utils.ExternalResource +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.math.absoluteValue +import kotlin.random.Random + +internal sealed class CommonOidbResponse : Packet { + data class Failure( + val result: Int, + val msg: String, + val e: Throwable?, + ) : CommonOidbResponse() { + inline fun createException(actionName: String): IllegalStateException { + return IllegalStateException("Failed $actionName, result=$result, msg=$msg", e) + } + + override fun toString(): String { + return "CommonOidbResponse.Failure(result=$result, msg=$msg, e=$e)" + } + } + + class Success( + val resp: T + ) : CommonOidbResponse() { + override fun toString(): String { + return "CommonOidbResponse.Success" + } + } +} + +internal interface CheckableStruct { + val int32RetCode: Int + val retMsg: String +} + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "RESULT_CLASS_IN_RETURN_TYPE") +@kotlin.internal.InlineOnly +internal inline fun CommonOidbResponse.toResult(actionName: String, checkResp: Boolean = true): Result { + return if (this is CommonOidbResponse.Failure) { + Result.failure(this.createException(actionName)) + } else { + this as CommonOidbResponse.Success + if (!checkResp) return Result.success(this.resp) + val result = this.resp + if (result is CheckableStruct) { + if (result.int32RetCode != 0) 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 + */ +internal inline fun ByteReadPacket.readOidbRespCommon( + bodyBufferDeserializer: DeserializationStrategy, + respMapper: (T) -> R +): CommonOidbResponse { + contract { callsInPlace(respMapper, InvocationKind.AT_MOST_ONCE) } + val oidb = readOidbSsoPkg(bodyBufferDeserializer) + return oidb.fold( + onSuccess = { + CommonOidbResponse.Success(kotlin.runCatching { + respMapper(this) + }.getOrElse { + return CommonOidbResponse.Failure(0, it.message ?: "", it) + }) + }, + onFailure = { + CommonOidbResponse.Failure(result, errorMsg, null) + } + ) +} + +internal object FileManagement { + val factories = arrayOf( + GetFileList, + GetFileInfo, + RequestDownload, + RequestUpload, + DeleteFile, + MoveFile, + RenameFile, + TransferFile, + Feed, + RenameFolder, +// MoveFolder, + DeleteFolder, + CreateFolder, + ) + + object GetFileList : OutgoingPacketFactory>("OidbSvc.0x6d8_1") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + folderId: String, + startIndex: Int, + ) = buildOutgoingUniPacket(client) { + writeOidb( + 1752, + 1, + Oidb0x6d8.ReqBody.serializer(), + Oidb0x6d8.ReqBody( + fileListInfoReq = Oidb0x6d8.GetFileListReqBody( + groupCode = groupCode, + appId = 3, + folderId = folderId, + fileCount = 20, + reqFrom = 3, + sortBy = 1, + startIndex = startIndex + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d8.RspBody.serializer()) { it.fileListInfoRsp!! } + } + } + + object GetFileInfo : OutgoingPacketFactory>("OidbSvc.0x6d8_0") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + fileId: String, + busId: Int, + ) = buildOutgoingUniPacket(client) { + writeOidb( + 1752, + 0, + Oidb0x6d8.ReqBody.serializer(), + Oidb0x6d8.ReqBody( + fileInfoReq = Oidb0x6d8.GetFileInfoReqBody( + groupCode = groupCode, + appId = 3, + fileId = fileId, + busId = busId + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d8.RspBody.serializer()) { it.fileInfoRsp!! } + } + } + + object RequestUpload : OutgoingPacketFactory>("OidbSvc.0x6d6_0") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + folderId: String, + resource: ExternalResource, + filename: String, + ) = buildOutgoingUniPacket(client) { + resource.sha1 // check supported + + writeOidb( + command = 1750, + serviceType = 0, + Oidb0x6d6.ReqBody.serializer(), + Oidb0x6d6.ReqBody( + uploadFileReq = Oidb0x6d6.UploadFileReqBody( + groupCode = groupCode, + appId = 3, + busId = 102, + entrance = 5, + parentFolderId = folderId, + fileName = filename, + localPath = "/storage/emulated/0/Pictures/files/s/$filename", + fileSize = resource.size, + sha = resource.sha1, + md5 = resource.md5, + boolSupportMultiUpload = true, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.uploadFileRsp!! } + } + } + + object RequestDownload : + OutgoingPacketFactory>("OidbSvc.0x6d6_2") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1750, + serviceType = 2, + Oidb0x6d6.ReqBody.serializer(), + Oidb0x6d6.ReqBody( + downloadFileReq = Oidb0x6d6.DownloadFileReqBody( + groupCode = groupCode, + appId = 3, + busId = busId, + fileId = fileId, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.downloadFileRsp!! } + } + } + + object MoveFile : OutgoingPacketFactory>("OidbSvc.0x6d6_5") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + parentFolderId: String, + destFolderId: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1750, + serviceType = 5, + Oidb0x6d6.ReqBody.serializer(), + Oidb0x6d6.ReqBody( + moveFileReq = Oidb0x6d6.MoveFileReqBody( + groupCode = groupCode, + appId = 3, + busId = busId, + fileId = fileId, + parentFolderId = parentFolderId, + destFolderId = destFolderId + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.moveFileRsp!! } + } + } + + + // 转发 + object TransferFile : OutgoingPacketFactory>("OidbSvc.0x6d9_0") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1753, + serviceType = 0, + Oidb0x6d9.ReqBody.serializer(), + Oidb0x6d9.ReqBody( + transFileReq = Oidb0x6d9.TransFileReqBody( + groupCode = groupCode, + appId = 3, + busId = busId, + fileId = fileId, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d9.RspBody.serializer()) { it.transFileRsp!! } + } + } + + + object RenameFile : OutgoingPacketFactory>("OidbSvc.0x6d6_4") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + parentFolderId: String, + newName: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1750, + serviceType = 4, + Oidb0x6d6.ReqBody.serializer(), + Oidb0x6d6.ReqBody( + renameFileReq = Oidb0x6d6.RenameFileReqBody( + groupCode = groupCode, + appId = 3, + busId = busId, + fileId = fileId, + parentFolderId = parentFolderId, + newFileName = newName, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.renameFileRsp!! } + } + } + + object DeleteFile : OutgoingPacketFactory>("OidbSvc.0x6d6_3") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + parentFolderId: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1750, + serviceType = 3, + Oidb0x6d6.ReqBody.serializer(), + Oidb0x6d6.ReqBody( + deleteFileReq = Oidb0x6d6.DeleteFileReqBody( + groupCode = groupCode, + appId = 3, + busId = busId, + fileId = fileId, + parentFolderId = parentFolderId, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.deleteFileRsp!! } + } + } + + object Feed : OutgoingPacketFactory>("OidbSvc.0x6d9_4") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + busId: Int, + fileId: String, + random: Int = Random.nextInt().absoluteValue, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1753, + serviceType = 4, + Oidb0x6d9.ReqBody.serializer(), + Oidb0x6d9.ReqBody( + feedsInfoReq = Oidb0x6d9.FeedsReqBody( + groupCode = groupCode, + appId = 3, + feedsInfoList = listOf( + GroupFileCommon.FeedsInfo( + busId = busId, + fileId = fileId, + feedFlag = 1, + msgRandom = random, + ) + ) + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d9.RspBody.serializer()) { it.feedsInfoRsp!! } + } + } + + + object RenameFolder : OutgoingPacketFactory>("OidbSvc.0x6d7_2") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + folderId: String, + newName: String + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1751, + serviceType = 2, + Oidb0x6d7.ReqBody.serializer(), + Oidb0x6d7.ReqBody( + renameFolderReq = Oidb0x6d7.RenameFolderReqBody( + groupCode = groupCode, + appId = 3, + folderId = folderId, + newFolderName = newName, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.renameFolderRsp!! } + } + } + + // qq doesn't support +// object MoveFolder : OutgoingPacketFactory>("OidbSvc.0x6d7_3") { +// operator fun invoke( +// client: QQAndroidClient, +// groupCode: Long, +// folderId: String, +// parentFolderId: String, +// newParentFolderId: String, +// ) = buildOutgoingUniPacket(client) { +// writeOidb( +// command = 1751, +// serviceType = 3, +// Oidb0x6d7.ReqBody.serializer(), +// Oidb0x6d7.ReqBody( +// moveFolderReq = Oidb0x6d7.MoveFolderReqBody( +// groupCode = groupCode, +// appId = 3, +// folderId = folderId, +// parentFolderId = parentFolderId, +// destFolderId = newParentFolderId, +// ) +// ) +// ) +// } +// +// override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { +// return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.moveFolderRsp!! } +// } +// } + + object DeleteFolder : OutgoingPacketFactory>("OidbSvc.0x6d7_1") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + folderId: String, + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1751, + serviceType = 1, + Oidb0x6d7.ReqBody.serializer(), + Oidb0x6d7.ReqBody( + deleteFolderReq = Oidb0x6d7.DeleteFolderReqBody( + groupCode = groupCode, + appId = 3, + folderId = folderId, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.deleteFolderRsp!! } + } + } + + object CreateFolder : OutgoingPacketFactory>("OidbSvc.0x6d7_0") { + operator fun invoke( + client: QQAndroidClient, + groupCode: Long, + parentFolderId: String, + name: String + ) = buildOutgoingUniPacket(client) { + writeOidb( + command = 1751, + serviceType = 0, + Oidb0x6d7.ReqBody.serializer(), + Oidb0x6d7.ReqBody( + createFolderReq = Oidb0x6d7.CreateFolderReqBody( + groupCode = groupCode, + appId = 3, + parentFolderId = parentFolderId, + folderName = name, + ) + ) + ) + } + + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): CommonOidbResponse { + return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.createFolderRsp!! } + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt index 74f273804..948e05fa0 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt @@ -238,10 +238,11 @@ internal class TroopManagement { OidbSso.OIDBSSOPkg.serializer(), OidbSso.OIDBSSOPkg( command = 2202, + serviceType = 0, bodybuffer = Oidb0x89a.ReqBody( groupCode = groupCode, stGroupInfo = Oidb0x89a.Groupinfo().apply(info) - ).toByteArray(Oidb0x89a.ReqBody.serializer()) + ).toByteArray(Oidb0x89a.ReqBody.serializer()), ) ) } diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt index 4db27e5f7..490b042a7 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt @@ -9,6 +9,8 @@ package net.mamoe.mirai.internal.network.protocol.packet.chat.receive +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.toByteArray import net.mamoe.mirai.contact.Friend @@ -469,7 +471,7 @@ internal inline fun MessageSvcPbSendMsg.createToTemp( member: Member, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToTempImpl) -> Unit + crossinline sourceCallback: (Deferred) -> Unit ): List { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -482,7 +484,7 @@ internal inline fun MessageSvcPbSendMsg.createToTemp( sequenceIds = intArrayOf(client.atomicNextMessageSequenceId()), originalMessage = message ) - sourceCallback(source) + sourceCallback(CompletableDeferred(source)) return createToTempImpl( client, member, @@ -497,7 +499,7 @@ internal inline fun MessageSvcPbSendMsg.createToStranger( stranger: Stranger, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToStrangerImpl) -> Unit + crossinline sourceCallback: (Deferred) -> Unit ): List { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -506,9 +508,8 @@ internal inline fun MessageSvcPbSendMsg.createToStranger( client, stranger, message, - fragmented, - sourceCallback - ) + fragmented + ) { sourceCallback(CompletableDeferred(it)) } } internal inline fun MessageSvcPbSendMsg.createToFriend( @@ -516,7 +517,7 @@ internal inline fun MessageSvcPbSendMsg.createToFriend( qq: Friend, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToFriendImpl) -> Unit + crossinline sourceCallback: (Deferred) -> Unit ): List { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -525,9 +526,8 @@ internal inline fun MessageSvcPbSendMsg.createToFriend( client, qq, message, - fragmented, - sourceCallback - ) + fragmented + ) { sourceCallback(CompletableDeferred(it)) } } @@ -536,7 +536,7 @@ internal inline fun MessageSvcPbSendMsg.createToGroup( group: Group, message: MessageChain, fragmented: Boolean, - crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit + crossinline sourceCallback: (Deferred) -> Unit ): List { contract { callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE) @@ -545,7 +545,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup( client, group, message, - fragmented, - sourceCallback - ) + fragmented + ) { sourceCallback(CompletableDeferred(it)) } } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt index 543006133..9e6e2c0c4 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt @@ -67,7 +67,9 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin val messageRandom = pbPushMsg.msg.msgBody.richText.attr?.random ?: return null if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom } - || msgHead.fromAppid == 3116) { + || msgHead.fromAppid == 3116 || msgHead.fromAppid == 2021) { + // 3116=group music share + // 2021=group file // message sent by bot return SendGroupMessageReceipt( messageRandom, @@ -103,7 +105,7 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory("Onlin val name: String if (anonymous != null) { // anonymous member - sender = group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeToBase64()) + sender = group.newAnonymous(anonymous.anonNick.encodeToString(), anonymous.anonId.encodeBase64()) name = sender.nameCard } else { // normal member chat sender = group[msgHead.fromUin] as NormalMemberImpl? ?: kotlin.run { diff --git a/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt b/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt index 0a85038ea..3e14d9537 100644 --- a/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt +++ b/mirai-core/src/commonMain/kotlin/utils/PlatformSocket.kt @@ -16,6 +16,7 @@ import kotlinx.io.core.ByteReadPacket import kotlinx.io.core.Closeable import kotlinx.io.streams.readPacketAtMost import kotlinx.io.streams.writePacket +import net.mamoe.mirai.internal.network.highway.HighwayProtocolChannel import net.mamoe.mirai.utils.withUse import java.io.BufferedInputStream import java.io.BufferedOutputStream @@ -30,7 +31,7 @@ import kotlin.contracts.contract /** * TCP Socket. */ -internal class PlatformSocket : Closeable { +internal class PlatformSocket : Closeable, HighwayProtocolChannel { private lateinit var socket: Socket val isOpen: Boolean @@ -64,7 +65,7 @@ internal class PlatformSocket : Closeable { /** * @throws SendPacketInternalException */ - suspend fun send(packet: ByteReadPacket) { + override suspend fun send(packet: ByteReadPacket) { runInterruptible(Dispatchers.IO) { try { writeChannel.writePacket(packet) @@ -80,7 +81,7 @@ internal class PlatformSocket : Closeable { /** * @throws ReadPacketInternalException */ - suspend fun read(): ByteReadPacket = suspendCancellableCoroutine { cont -> + override suspend fun read(): ByteReadPacket = suspendCancellableCoroutine { cont -> val task = thread.submit { kotlin.runCatching { readChannel.readPacketAtMost(Long.MAX_VALUE) diff --git a/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt b/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt new file mode 100644 index 000000000..0e4e455b6 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/utils/RemoteFileImpl.kt @@ -0,0 +1,537 @@ +/* + * 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/master/LICENSE + */ + +package net.mamoe.mirai.internal.utils + +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.utils.io.* +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.EMPTY_BYTE_ARRAY +import net.mamoe.mirai.internal.asQQAndroidBot +import net.mamoe.mirai.internal.contact.groupCode +import net.mamoe.mirai.internal.message.FileMessageImpl +import net.mamoe.mirai.internal.network.highway.Highway +import net.mamoe.mirai.internal.network.highway.ResourceKind +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.io.serialization.toByteArray +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.* +import net.mamoe.mirai.utils.RemoteFile.Companion.ROOT_PATH +import java.util.* +import kotlin.contracts.contract + +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 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(): RemoteFileImpl { + contract { returns() implies (this@checkIsImpl is RemoteFileImpl) } + return this as? RemoteFileImpl ?: error("RemoteFile must not be implemented manually.") +} + +internal class RemoteFileImpl( + contact: Group, + override val path: String, // absolute +) : RemoteFile { + private val contactRef by contact.weakRef() + override val contact get() = contactRef ?: error("RemoteFile is closed due to Contact closed.") + + constructor(contact: Group, parent: String, name: String) : this(contact, fs.normalize(parent, name)) + + 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: RemoteFileImpl? + get() { + if (path == ROOT_PATH) return null + val s = path.substringBeforeLast('/') + return RemoteFileImpl(contact, if (s.isEmpty()) ROOT_PATH else s) + } + + /** + * Prefer id matching. + */ + private suspend fun Flow.findMatching(): Oidb0x6d8.GetFileListRspBody.Item? { + var nameMatching: Oidb0x6d8.GetFileListRspBody.Item? = null + + val idMatching = firstOrNull { + if (it.name == this@RemoteFileImpl.name) { + nameMatching = it + } + it.id == this@RemoteFileImpl.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 + 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 fun getFilesFlow(): Flow { + return flow { + var index = 0 + while (true) { + val list = FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = path, + startIndex = index + ).sendAndExpect(bot).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 { + return getFilesFlow().mapNotNull { item -> + item.resolveToFile() + } + } + + @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") + @OptIn(JavaFriendlyAPI::class) + override suspend fun listFilesIterator(lazy: Boolean): Iterator { + if (!lazy) return listFiles().toList().iterator() + + return object : Iterator { + private val queue = ArrayDeque(1) + + @Volatile + private var index = 0 + private var ended = false + + private suspend fun updateItems() { + val list = FileManagement.GetFileList( + client, + groupCode = contact.id, + folderId = path, + startIndex = index + ).sendAndExpect(bot).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? { + return getFilesFlow().filter { it.id == id }.firstOrNull()?.resolveToFile() + } + + 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 -> { + FileManagement.DeleteFile( + client, + groupCode = contact.id, + busId = info.busId, + fileId = info.id, + parentFolderId = info.parentFolderId, + ).sendAndExpect(bot).toResult("RemoteFile.delete", checkResp = false).getOrThrow().int32RetCode == 0 + } +// recursively -> { +// this.listFiles().collect { child -> +// child.delete() +// } +// this.delete() +// } + else -> { + // natively 'recursive' + FileManagement.DeleteFolder( + client, contact.id, info.id + ).sendAndExpect(bot).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 if (info.isFile) { + FileManagement.RenameFile(client, contact.id, info.busId, info.id, info.parentFolderId, normalized) + } else { + FileManagement.RenameFolder(client, contact.id, info.id, normalized) + }.sendAndExpect(bot).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 + FileManagement.MoveFile(client, contact.id, info.busId, info.id, info.parentFolderId, newParentId) + .sendAndExpect(bot).toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 + } else { + return FileManagement.RenameFolder(client, contact.id, info.id, target.name).sendAndExpect(bot) + .toResult("RemoteFile.moveTo", checkResp = false).getOrThrow().int32RetCode == 0 + } + } + + + override suspend fun moveTo(path: String): Boolean = moveTo(resolve(path)) + override suspend fun mkdir(): Boolean { + if (path == ROOT_PATH) return false + if (!isBotOperator()) return false + + val parentFolderId: String = parent?.getIdSmart() ?: return false + + return FileManagement.CreateFolder(client, contact.id, parentFolderId, this.name) + .sendAndExpect(bot).toResult("RemoteFile.mkdir", checkResp = false).getOrThrow().int32RetCode == 0 + } + + override suspend fun upload(resource: ExternalResource, callback: RemoteFile.ProgressionCallback?): Boolean { + val parent = parent ?: return false + val parentInfo = parent.getFileFolderInfo() ?: return false + val resp = FileManagement.RequestUpload( + client, + groupCode = contact.id, + folderId = parentInfo.id, + resource = resource, + filename = this.name + ).sendAndExpect(bot).toResult("RemoteFile.upload").getOrThrow() + if (resp.boolFileExist) { + return true + } + + 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 true + } + + override suspend fun uploadAndSend(resource: ExternalResource): MessageReceipt { + if (!upload(resource)) error("Failed to upload file.") + return toMessage()?.sendTo(contact) ?: error("Failed to create FileMessage") + } + +// override suspend fun writeSession(resource: ExternalResource): FileUploadSession { +// } + + override suspend fun getDownloadInfo(): RemoteFile.DownloadInfo? { + val info = getFileFolderInfo() ?: return null + if (!info.isFile) return null + val resp = FileManagement.RequestDownload( + client, + groupCode = contact.id, + busId = info.busId, + fileId = info.id + ).sendAndExpect(bot).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(name, info.id, info.size, info.busId) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt index ae71df0fb..f66b04b1e 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt +++ b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt @@ -9,6 +9,7 @@ @file:JvmName("SerializationUtils") @file:JvmMultifileClass +@file:Suppress("NOTHING_TO_INLINE") package net.mamoe.mirai.internal.utils.io.serialization @@ -127,6 +128,24 @@ internal fun BytePacketBuilder.writeProtoBuf(serializer: Serializ this.writeFully(v.toByteArray(serializer)) } +internal fun BytePacketBuilder.writeOidb( + command: Int = 0, + serviceType: Int = 0, + serializer: SerializationStrategy, + v: T, + clientVersion: String = "android 8.4.8", +) { + return this.writeProtoBuf( + OidbSso.OIDBSSOPkg.serializer(), + OidbSso.OIDBSSOPkg( + command = command, + serviceType = serviceType, + clientVersion = clientVersion, + bodybuffer = v.toByteArray(serializer) + ) + ) +} + /** * dump */ @@ -157,6 +176,52 @@ internal fun ByteReadPacket.readProtoBuf( length: Int = this.remaining.toInt() ): T = KtProtoBuf.decodeFromByteArray(serializer, this.readBytes(length)) +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") +internal inline class OidbBodyOrFailure private constructor( + private val v: Any +) { + internal class Failure( + val oidb: OidbSso.OIDBSSOPkg + ) + + inline fun fold( + onSuccess: T.(T) -> R, + onFailure: OidbSso.OIDBSSOPkg.(OidbSso.OIDBSSOPkg) -> R, + ): R { + contract { + callsInPlace(onSuccess, InvocationKind.AT_MOST_ONCE) + callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE) + } + @Suppress("UNCHECKED_CAST") + return if (v is Failure) { + onFailure(v.oidb, v.oidb) + } else { + val t = v as T + onSuccess(t, t) + } + } + + companion object { + fun success(t: T): OidbBodyOrFailure = OidbBodyOrFailure(t) + fun failure(oidb: OidbSso.OIDBSSOPkg): OidbBodyOrFailure = OidbBodyOrFailure(Failure(oidb)) + } +} + +/** + * load + */ +internal inline fun ByteReadPacket.readOidbSsoPkg( + serializer: DeserializationStrategy, + length: Int = this.remaining.toInt() +): OidbBodyOrFailure { + val oidb = readBytes(length).loadAs(OidbSso.OIDBSSOPkg.serializer()) + return if (oidb.result == 0) { + OidbBodyOrFailure.success(oidb.bodybuffer.loadAs(serializer)) + } else { + OidbBodyOrFailure.failure(oidb) + } +} + /** * 构造 [RequestPacket] 的 [RequestPacket.sBuffer] */ diff --git a/mirai-core/src/commonMain/kotlin/utils/retryWithServers.kt b/mirai-core/src/commonMain/kotlin/utils/retryWithServers.kt index 0e0be66e4..83399a3d3 100644 --- a/mirai-core/src/commonMain/kotlin/utils/retryWithServers.kt +++ b/mirai-core/src/commonMain/kotlin/utils/retryWithServers.kt @@ -10,10 +10,11 @@ package net.mamoe.mirai.internal.utils import kotlinx.coroutines.withTimeoutOrNull +import net.mamoe.mirai.utils.cast import kotlin.math.roundToInt -internal suspend inline fun Collection>.retryWithServers( +internal suspend inline fun Collection>.retryWithServers( timeoutMillis: Long, onFail: (exception: Throwable?) -> Nothing, crossinline block: suspend (ip: String, port: Int) -> R @@ -24,7 +25,11 @@ internal suspend inline fun Collection>.retryWithServers( for (pair in this) { return kotlin.runCatching { withTimeoutOrNull(timeoutMillis) { - block(pair.first.toIpV4AddressString(), pair.second) + if (IP::class == Int::class) { + block(pair.first.cast().toIpV4AddressString(), pair.second) + } else { + block(pair.first.toString(), pair.second) + } } }.recover { if (exception != null) { diff --git a/mirai-core/src/commonTest/kotlin/internal/utils/FileSystemTest.kt b/mirai-core/src/commonTest/kotlin/internal/utils/FileSystemTest.kt new file mode 100644 index 000000000..69c6c06ff --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/internal/utils/FileSystemTest.kt @@ -0,0 +1,40 @@ +/* + * 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/master/LICENSE + */ + + +package net.mamoe.mirai.internal.utils + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +internal class FileSystemTest { + + private val fs = FileSystem + + @Test + fun testLegitimacy() { + fs.checkLegitimacy("a") + assertFailsWith { fs.checkLegitimacy("a:") } + assertFailsWith { fs.checkLegitimacy("?a") } + } + + @Test + fun testNormalize() { + assertEquals("/", fs.normalize("/")) + assertEquals("/", fs.normalize("\\")) + assertEquals("/foo", fs.normalize("/foo")) + assertEquals("/foo", fs.normalize("\\foo")) + assertEquals("foo", fs.normalize("foo")) + assertEquals("foo/", fs.normalize("foo/")) + + assertEquals("/bar", fs.normalize("\\foo", "/bar")) + assertEquals("/foo/bar", fs.normalize("\\foo", "bar")) + } +} \ No newline at end of file