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
This commit is contained in:
Him188 2021-03-08 18:34:45 +08:00 committed by GitHub
parent 716b8c6129
commit e256ec06d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 3000 additions and 215 deletions

View File

@ -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 <init> (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 <init> (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

View File

@ -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 <init> (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 <init> (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

View File

@ -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

View File

@ -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`)

View File

@ -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
}

View File

@ -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"
/**
* 群成员列表, 不含机器人自己, 含群主.
*

View File

@ -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

View File

@ -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, ForwardMessage>(MessageContent, { it.safeCast() }) {
public const val SERIAL_NAME: String = "FileMessage"
}
}

View File

@ -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].

View File

@ -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] 都拥有 [RemoteFile.id] 用于区分重名文件或目录. [RemoteFile] 表示一个文件时返回空迭代器.
* @param lazy `true` 时惰性获取, `false` 时立即获取全部文件列表.
*/
@JavaFriendlyAPI
public suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile>
/**
* 得到相应文件消息, 可以发送. [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<Long>.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<Contact>
/**
* 上传文件并发送文件消息.
* @see uploadAndSend
*/
@MiraiExperimentalApi
public suspend fun uploadAndSend(file: File): MessageReceipt<Contact> =
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 : FileSupported> C.uploadFileAndSend(
path: String,
resource: ExternalResource
): MessageReceipt<C> {
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.")
}
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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() }

View File

@ -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)
}

View File

@ -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

View File

@ -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<NormalMember> = ContactList(members.mapNotNullTo(ConcurrentLinkedQueue()) {
if (it.uin == bot.id) {
botAsMember = newMember(it).cast()

View File

@ -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<C : Contact> {
val group = contact
var source: OnlineMessageSource.Outgoing? = null
var source: Deferred<OnlineMessageSource.Outgoing>? = null
bot.network.run {
sendMessageMultiProtocol(
@ -162,34 +166,37 @@ internal abstract class SendMessageHandler<C : Contact> {
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<OnlineMessageSource.Outgoing>) -> Unit
): List<OutgoingPacket> {
message.takeSingleContent<MusicShare>()?.let { musicShare ->
return listOf(
@ -200,6 +207,12 @@ internal abstract class SendMessageHandler<C : Contact> {
)
}
message.takeSingleContent<FileMessage>()?.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<C : Contact> {
contact: C,
message: MessageChain,
fragmented: Boolean,
sourceCallback: (OnlineMessageSource.Outgoing) -> Unit,
sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit,
) -> List<OutgoingPacket>
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<C : AbstractUser>(
) : SendMessageHandler<C>() {
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<FriendImpl>(contact) {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: FriendImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
override val messageSvcSendMessage: (client: QQAndroidClient, contact: FriendImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToFriend
}
internal class StrangerSendMessageHandler(
contact: StrangerImpl,
) : UserSendMessageHandler<StrangerImpl>(contact) {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
override val messageSvcSendMessage: (client: QQAndroidClient, contact: StrangerImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToStranger
}
internal class GroupTempSendMessageHandler(
contact: NormalMemberImpl,
) : UserSendMessageHandler<NormalMemberImpl>(contact) {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: NormalMemberImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
override val messageSvcSendMessage: (client: QQAndroidClient, contact: NormalMemberImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
MessageSvcPbSendMsg::createToTemp
}
internal class GroupSendMessageHandler(
override val contact: GroupImpl,
) : SendMessageHandler<GroupImpl>() {
override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (OnlineMessageSource.Outgoing) -> Unit) -> List<OutgoingPacket> =
override val messageSvcSendMessage: (client: QQAndroidClient, contact: GroupImpl, message: MessageChain, fragmented: Boolean, sourceCallback: (Deferred<OnlineMessageSource.Outgoing>) -> Unit) -> List<OutgoingPacket> =
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(

View File

@ -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<GroupImpl>().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 <T : User> Friend.sendMessageImpl(
message: Message,
friendReceiptConstructor: (OnlineMessageSourceToFriendImpl) -> MessageReceipt<Friend>,
tReceiptConstructor: (OnlineMessageSourceToFriendImpl) -> MessageReceipt<T>
): MessageReceipt<T> {
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<QuoteReply>()?.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<MessageSvcPbSendMsg.Response>().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 <T : User> Stranger.sendMessageImpl(
message: Message,
strangerReceiptConstructor: (OnlineMessageSourceToStrangerImpl) -> MessageReceipt<Stranger>,
tReceiptConstructor: (OnlineMessageSourceToStrangerImpl) -> MessageReceipt<T>
): MessageReceipt<T> {
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<QuoteReply>()?.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<net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg.Response>()
.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())

View File

@ -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
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -23,7 +23,8 @@ import kotlin.contracts.contract
internal class ChunkedFlowSession<T>(
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<T>(
val size = runBIO { input.read(buffer) }
if (size == -1) return
block(mapper(buffer, size, offset.getAndAdd(size.toLongUnsigned())))
callback?.onProgression(offset.get())
}
}
}

View File

@ -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 <reified R> tryServersUpload(
internal suspend inline fun <reified R, reified IP> tryServersUpload(
bot: QQAndroidBot,
servers: Collection<Pair<Int, Int>>,
servers: Collection<Pair<IP, Int>>,
resourceSize: Long,
resourceKind: ResourceKind,
channelKind: ChannelKind,
@ -250,10 +267,51 @@ private fun <T> Flow<T>.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<ByteReadPacket>.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<ByteReadPacket>.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 <E : Any> ReceiveChannel<E>.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<ByteReadPacket> {
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,

View File

@ -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<ExcitingHostInfo>,
) : 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

View File

@ -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<FileInfo> = 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<FolderInfo> = emptyList()
) : ProtoBuf
@Serializable
internal class OverwriteInfo(
@JvmField @ProtoNumber(1) val fileId: String = "",
@JvmField @ProtoNumber(2) val downloadTimes: Int = 0
) : ProtoBuf
}

View File

@ -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,

View File

@ -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,

View File

@ -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
}

View File

@ -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 = "",

View File

@ -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<String> = emptyList(),
@JvmField @ProtoNumber(13) val uploadIpLanV6: List<String> = emptyList(),
@JvmField @ProtoNumber(14) val uploadPort: Int = 0
) : ProtoBuf, CheckableStruct
}

View File

@ -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
}

View File

@ -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<Item> = 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
}

View File

@ -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<GroupFileCommon.FeedsInfo> = 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<GroupFileCommon.FeedsResult> = 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
}

View File

@ -161,6 +161,7 @@ internal object KnownPacketFactories {
StrangerList.DelStranger,
SummaryCard.ReqSummaryCard,
MusicSharePacket,
*FileManagement.factories
)
object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf(

View File

@ -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<T> : Packet {
data class Failure<T>(
val result: Int,
val msg: String,
val e: Throwable?,
) : CommonOidbResponse<T>() {
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<T>(
val resp: T
) : CommonOidbResponse<T>() {
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 <T> CommonOidbResponse<T>.toResult(actionName: String, checkResp: Boolean = true): Result<T> {
return if (this is CommonOidbResponse.Failure) {
Result.failure(this.createException(actionName))
} else {
this as CommonOidbResponse.Success<T>
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 <T : ProtoBuf, R> ByteReadPacket.readOidbRespCommon(
bodyBufferDeserializer: DeserializationStrategy<T>,
respMapper: (T) -> R
): CommonOidbResponse<R> {
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<CommonOidbResponse<Oidb0x6d8.GetFileListRspBody>>("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<Oidb0x6d8.GetFileListRspBody> {
return readOidbRespCommon(Oidb0x6d8.RspBody.serializer()) { it.fileListInfoRsp!! }
}
}
object GetFileInfo : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d8.GetFileInfoRspBody>>("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<Oidb0x6d8.GetFileInfoRspBody> {
return readOidbRespCommon(Oidb0x6d8.RspBody.serializer()) { it.fileInfoRsp!! }
}
}
object RequestUpload : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d6.UploadFileRspBody>>("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<Oidb0x6d6.UploadFileRspBody> {
return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.uploadFileRsp!! }
}
}
object RequestDownload :
OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d6.DownloadFileRspBody>>("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<Oidb0x6d6.DownloadFileRspBody> {
return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.downloadFileRsp!! }
}
}
object MoveFile : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d6.MoveFileRspBody>>("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<Oidb0x6d6.MoveFileRspBody> {
return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.moveFileRsp!! }
}
}
// 转发
object TransferFile : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d9.TransFileRspBody>>("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<Oidb0x6d9.TransFileRspBody> {
return readOidbRespCommon(Oidb0x6d9.RspBody.serializer()) { it.transFileRsp!! }
}
}
object RenameFile : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d6.RenameFileRspBody>>("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<Oidb0x6d6.RenameFileRspBody> {
return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.renameFileRsp!! }
}
}
object DeleteFile : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d6.DeleteFileRspBody>>("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<Oidb0x6d6.DeleteFileRspBody> {
return readOidbRespCommon(Oidb0x6d6.RspBody.serializer()) { it.deleteFileRsp!! }
}
}
object Feed : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d9.FeedsRspBody>>("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<Oidb0x6d9.FeedsRspBody> {
return readOidbRespCommon(Oidb0x6d9.RspBody.serializer()) { it.feedsInfoRsp!! }
}
}
object RenameFolder : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d7.RenameFolderRspBody>>("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<Oidb0x6d7.RenameFolderRspBody> {
return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.renameFolderRsp!! }
}
}
// qq doesn't support
// object MoveFolder : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d7.MoveFolderRspBody>>("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<Oidb0x6d7.MoveFolderRspBody> {
// return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.moveFolderRsp!! }
// }
// }
object DeleteFolder : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d7.DeleteFolderRspBody>>("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<Oidb0x6d7.DeleteFolderRspBody> {
return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.deleteFolderRsp!! }
}
}
object CreateFolder : OutgoingPacketFactory<CommonOidbResponse<Oidb0x6d7.CreateFolderRspBody>>("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<Oidb0x6d7.CreateFolderRspBody> {
return readOidbRespCommon(Oidb0x6d7.RspBody.serializer()) { it.createFolderRsp!! }
}
}
}

View File

@ -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()),
)
)
}

View File

@ -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<OnlineMessageSourceToTempImpl>) -> Unit
): List<OutgoingPacket> {
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<OnlineMessageSourceToStrangerImpl>) -> Unit
): List<OutgoingPacket> {
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<OnlineMessageSourceToFriendImpl>) -> Unit
): List<OutgoingPacket> {
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<OnlineMessageSourceToGroupImpl>) -> Unit
): List<OutgoingPacket> {
contract {
callsInPlace(sourceCallback, InvocationKind.EXACTLY_ONCE)
@ -545,7 +545,6 @@ internal inline fun MessageSvcPbSendMsg.createToGroup(
client,
group,
message,
fragmented,
sourceCallback
)
fragmented
) { sourceCallback(CompletableDeferred(it)) }
}

View File

@ -67,7 +67,9 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("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<Packet?>("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 {

View File

@ -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)

View File

@ -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<Oidb0x6d8.GetFileListRspBody.Item>.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<Oidb0x6d8.GetFileListRspBody.Item> {
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<RemoteFile> {
return getFilesFlow().mapNotNull { item ->
item.resolveToFile()
}
}
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@OptIn(JavaFriendlyAPI::class)
override suspend fun listFilesIterator(lazy: Boolean): Iterator<RemoteFile> {
if (!lazy) return listFiles().toList().iterator()
return object : Iterator<RemoteFile> {
private val queue = ArrayDeque<Oidb0x6d8.GetFileListRspBody.Item>(1)
@Volatile
private var index = 0
private var ended = false
private suspend fun updateItems() {
val list = 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<Contact> {
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)
}
}

View File

@ -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 <T : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: Serializ
this.writeFully(v.toByteArray(serializer))
}
internal fun <T : ProtoBuf> BytePacketBuilder.writeOidb(
command: Int = 0,
serviceType: Int = 0,
serializer: SerializationStrategy<T>,
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 <T : ProtoBuf> 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<T : ProtoBuf> private constructor(
private val v: Any
) {
internal class Failure(
val oidb: OidbSso.OIDBSSOPkg
)
inline fun <R> 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 <T : ProtoBuf> success(t: T): OidbBodyOrFailure<T> = OidbBodyOrFailure(t)
fun <T : ProtoBuf> failure(oidb: OidbSso.OIDBSSOPkg): OidbBodyOrFailure<T> = OidbBodyOrFailure(Failure(oidb))
}
}
/**
* load
*/
internal inline fun <T : ProtoBuf> ByteReadPacket.readOidbSsoPkg(
serializer: DeserializationStrategy<T>,
length: Int = this.remaining.toInt()
): OidbBodyOrFailure<T> {
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]
*/

View File

@ -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 <R> Collection<Pair<Int, Int>>.retryWithServers(
internal suspend inline fun <R, reified IP> Collection<Pair<IP, Int>>.retryWithServers(
timeoutMillis: Long,
onFail: (exception: Throwable?) -> Nothing,
crossinline block: suspend (ip: String, port: Int) -> R
@ -24,7 +25,11 @@ internal suspend inline fun <R> Collection<Pair<Int, Int>>.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<Int>().toIpV4AddressString(), pair.second)
} else {
block(pair.first.toString(), pair.second)
}
}
}.recover {
if (exception != null) {

View File

@ -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<IllegalArgumentException> { fs.checkLegitimacy("a:") }
assertFailsWith<IllegalArgumentException> { 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"))
}
}