Bladeren bron

Merge branch 'dev' into 2.7-release

Him188 4 jaren geleden
bovenliggende
commit
21b85c344c
78 gewijzigde bestanden met toevoegingen van 2859 en 564 verwijderingen
  1. 128 31
      binary-compatibility-validator/android/api/binary-compatibility-validator-android.api
  2. 123 31
      binary-compatibility-validator/api/binary-compatibility-validator.api
  3. 11 3
      buildSrc/src/main/kotlin/Versions.kt
  4. 148 9
      docs/Bots.md
  5. 77 0
      logging/README.md
  6. 37 0
      logging/mirai-logging-log4j2/build.gradle.kts
  7. 10 0
      logging/mirai-logging-log4j2/resources/META-INF/services/net.mamoe.mirai.utils.MiraiLogger$Factory
  8. 30 0
      logging/mirai-logging-log4j2/src/MiraiLog4JFactory.kt
  9. 63 0
      logging/mirai-logging-log4j2/test/MiraiLog4JAdapterTest.kt
  10. 38 0
      logging/mirai-logging-slf4j-logback/build.gradle.kts
  11. 63 0
      logging/mirai-logging-slf4j-logback/test/MiraiSlf4JLogbackAdapterTest.kt
  12. 38 0
      logging/mirai-logging-slf4j-simple/build.gradle.kts
  13. 63 0
      logging/mirai-logging-slf4j-simple/test/MiraiSlf4JSimpleAdapterTest.kt
  14. 37 0
      logging/mirai-logging-slf4j/build.gradle.kts
  15. 62 0
      logging/mirai-logging-slf4j/test/MiraiSlf4JAdapterTest.kt
  16. 1 1
      mirai-console
  17. 8 2
      mirai-core-api/build.gradle.kts
  18. 0 16
      mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt
  19. 45 0
      mirai-core-api/src/commonMain/kotlin/contact/AudioSupported.kt
  20. 1 1
      mirai-core-api/src/commonMain/kotlin/contact/Friend.kt
  21. 13 1
      mirai-core-api/src/commonMain/kotlin/contact/Group.kt
  22. 14 0
      mirai-core-api/src/commonMain/kotlin/contact/NormalMember.kt
  23. 0 39
      mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt
  24. 1 1
      mirai-core-api/src/commonMain/kotlin/event/Event.kt
  25. 24 0
      mirai-core-api/src/commonMain/kotlin/event/subscribeMessages.kt
  26. 44 18
      mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt
  27. 1 1
      mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt
  28. 67 23
      mirai-core-api/src/commonMain/kotlin/internal/utils/LoggerAdapterImpls.kt
  29. 70 0
      mirai-core-api/src/commonMain/kotlin/internal/utils/MarkedMiraiLogger.kt
  30. 274 0
      mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt
  31. 79 78
      mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt
  32. 6 2
      mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt
  33. 1 0
      mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt
  34. 6 50
      mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt
  35. 52 6
      mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt
  36. 174 33
      mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt
  37. 91 0
      mirai-core-api/src/commonTest/kotlin/logging/Log4j2LoggingTest.kt
  38. 36 0
      mirai-core-api/src/commonTest/kotlin/logging/LoggingCompatibilityTest.kt
  39. 13 0
      mirai-core-api/src/commonTest/resources/log4j.properties
  40. 5 3
      mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt
  41. 23 0
      mirai-core-utils/src/androidMain/kotlin/Actuals.kt
  42. 2 2
      mirai-core-utils/src/commonMain/kotlin/Annotations.kt
  43. 1 1
      mirai-core-utils/src/commonMain/kotlin/Bytes.kt
  44. 49 0
      mirai-core-utils/src/commonMain/kotlin/ComputeOnNullMutableProperty.kt
  45. 22 0
      mirai-core-utils/src/commonMain/kotlin/Services.kt
  46. 51 0
      mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ComputeOnNullMutablePropertyTest.kt
  47. 22 0
      mirai-core-utils/src/jvmMain/kotlin/Actuals.kt
  48. 1 1
      mirai-core/build.gradle.kts
  49. 1 1
      mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt
  50. 8 10
      mirai-core/src/commonMain/kotlin/MiraiImpl.kt
  51. 37 35
      mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt
  52. 51 25
      mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt
  53. 6 3
      mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt
  54. 302 0
      mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt
  55. 12 13
      mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt
  56. 5 2
      mirai-core/src/commonMain/kotlin/message/imagesImpl.kt
  57. 1 1
      mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt
  58. 3 3
      mirai-core/src/commonMain/kotlin/network/components/ServerList.kt
  59. 2 1
      mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt
  60. 4 4
      mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt
  61. 2 2
      mirai-core/src/commonMain/kotlin/network/highway/Highway.kt
  62. 63 1
      mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt
  63. 3 2
      mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt
  64. 9 4
      mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt
  65. 7 4
      mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt
  66. 4 52
      mirai-core/src/commonMain/kotlin/utils/SubLogger.kt
  67. 2 1
      mirai-core/src/commonMain/kotlin/utils/contentToString.kt
  68. 10 0
      mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory
  69. 75 0
      mirai-core/src/commonTest/kotlin/message/AudioTest.kt
  70. 4 1
      mirai-core/src/commonTest/kotlin/network/component/EventDispatcherTest.kt
  71. 4 3
      mirai-core/src/commonTest/kotlin/network/framework/AbstractMockNetworkHandlerTest.kt
  72. 1 1
      mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt
  73. 3 3
      mirai-core/src/commonTest/kotlin/network/framework/TestNetworkHandlerContext.kt
  74. 1 1
      mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt
  75. 10 3
      mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt
  76. 6 5
      mirai-core/src/commonTest/kotlin/test/printing.kt
  77. 86 26
      mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt
  78. 12 4
      settings.gradle.kts

+ 128 - 31
binary-compatibility-validator/android/api/binary-compatibility-validator-android.api

@@ -176,6 +176,11 @@ public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/ma
 	public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
 
+public abstract interface class net/mamoe/mirai/contact/AudioSupported : net/mamoe/mirai/contact/Contact {
+	public fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public abstract fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
 public final class net/mamoe/mirai/contact/BotIsBeingMutedException : java/lang/RuntimeException {
 	public fun <init> (Lnet/mamoe/mirai/contact/Group;)V
 	public final fun getTarget ()Lnet/mamoe/mirai/contact/Group;
@@ -326,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
 	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, net/mamoe/mirai/contact/VoiceSupported {
+public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User {
 	public fun delete ()V
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
@@ -338,7 +343,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, net/mamoe/mirai/contact/FileSupported, net/mamoe/mirai/contact/VoiceSupported {
+public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, 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
@@ -365,6 +370,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;)Z
 	public abstract fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun setName (Ljava/lang/String;)V
+	public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice;
+	public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
 
 public final class net/mamoe/mirai/contact/Group$Companion {
@@ -446,6 +453,8 @@ public abstract interface class net/mamoe/mirai/contact/NormalMember : net/mamoe
 	public fun isMuted ()Z
 	public fun kick (Ljava/lang/String;)V
 	public abstract fun kick (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun kick (Ljava/lang/String;Z)V
+	public abstract fun kick (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun modifyAdmin (Z)V
 	public abstract fun modifyAdmin (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -558,11 +567,6 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi
 	public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
 }
 
-public abstract interface class net/mamoe/mirai/contact/VoiceSupported : net/mamoe/mirai/contact/Contact {
-	public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice;
-	public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-}
-
 public abstract interface class net/mamoe/mirai/contact/announcement/Announcement {
 	public static final field Companion Lnet/mamoe/mirai/contact/announcement/Announcement$Companion;
 	public abstract fun getContent ()Ljava/lang/String;
@@ -1754,6 +1758,8 @@ public final class net/mamoe/mirai/event/SubscribeMessagesKt {
 	public static synthetic fun subscribeStrangerMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
 	public static final fun subscribeTempMessages (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
 	public static synthetic fun subscribeTempMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
+	public static final fun subscribeUserMessages (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
+	public static synthetic fun subscribeUserMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
 }
 
 public final class net/mamoe/mirai/event/SyncFromEventKt {
@@ -3154,6 +3160,41 @@ public final class net/mamoe/mirai/message/data/AtAll : net/mamoe/mirai/message/
 	public fun toString ()Ljava/lang/String;
 }
 
+public abstract interface class net/mamoe/mirai/message/data/Audio : net/mamoe/mirai/message/data/MessageContent {
+	public static final field Key Lnet/mamoe/mirai/message/data/Audio$Key;
+	public fun contentToString ()Ljava/lang/String;
+	public abstract fun getCodec ()Lnet/mamoe/mirai/message/data/AudioCodec;
+	public abstract fun getExtraData ()[B
+	public abstract fun getFileMd5 ()[B
+	public abstract fun getFileSize ()J
+	public abstract fun getFilename ()Ljava/lang/String;
+	public abstract fun toString ()Ljava/lang/String;
+}
+
+public final class net/mamoe/mirai/message/data/Audio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+}
+
+public final class net/mamoe/mirai/message/data/AudioCodec : java/lang/Enum {
+	public static final field AMR Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final field Companion Lnet/mamoe/mirai/message/data/AudioCodec$Companion;
+	public static final field SILK Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun getFormatName ()Ljava/lang/String;
+	public final fun getId ()I
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static fun values ()[Lnet/mamoe/mirai/message/data/AudioCodec;
+}
+
+public final class net/mamoe/mirai/message/data/AudioCodec$Companion {
+	public final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+}
+
 public abstract interface class net/mamoe/mirai/message/data/ConstrainSingle : net/mamoe/mirai/message/data/SingleMessage {
 	public abstract fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
 }
@@ -4495,6 +4536,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun At (Lnet/mamoe/mirai/contact/UserOrBot;)Lnet/mamoe/mirai/message/data/At;
 	public static final synthetic fun FileMessage (Ljava/lang/String;ILjava/lang/String;J)Lnet/mamoe/mirai/message/data/FileMessage;
 	public static final synthetic fun Image (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image;
+	public static final synthetic fun OfflineAudio (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public static final synthetic fun OfflineAudio (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
 	public static final synthetic fun UnsupportedMessage ([B)Lnet/mamoe/mirai/message/data/UnsupportedMessage;
 	public static final synthetic fun at (Lnet/mamoe/mirai/contact/Member;)Lnet/mamoe/mirai/message/data/At;
 	public static final fun buildMessageChain (ILkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/MessageChain;
@@ -4537,6 +4580,7 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun toMessageChain ([Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/data/MessageChain;
 	public static final fun toOfflineMessageSource (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/OfflineMessageSource;
 	public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText;
+	public static final synthetic fun toVoice (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
 }
 
 public final class net/mamoe/mirai/message/data/MusicKind : java/lang/Enum {
@@ -4600,6 +4644,26 @@ public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public abstract interface class net/mamoe/mirai/message/data/OfflineAudio : net/mamoe/mirai/message/data/Audio {
+	public static final field Key Lnet/mamoe/mirai/message/data/OfflineAudio$Key;
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
+public abstract interface class net/mamoe/mirai/message/data/OfflineAudio$Factory {
+	public static final field INSTANCE Lnet/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE;
+	public abstract fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+}
+
+public final class net/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE : net/mamoe/mirai/message/data/OfflineAudio$Factory {
+	public fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+}
+
+public final class net/mamoe/mirai/message/data/OfflineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
 public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/mamoe/mirai/message/data/MessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OfflineMessageSource$Key;
 	public fun <init> ()V
@@ -4609,6 +4673,17 @@ public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/ma
 public final class net/mamoe/mirai/message/data/OfflineMessageSource$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
 }
 
+public abstract interface class net/mamoe/mirai/message/data/OnlineAudio : net/mamoe/mirai/message/data/Audio {
+	public static final field Key Lnet/mamoe/mirai/message/data/OnlineAudio$Key;
+	public static final field SERIAL_NAME Ljava/lang/String;
+	public abstract fun getLength ()J
+	public abstract fun getUrlForDownload ()Ljava/lang/String;
+}
+
+public final class net/mamoe/mirai/message/data/OnlineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mamoe/mirai/message/data/MessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Key;
 	public abstract fun getBot ()Lnet/mamoe/mirai/Bot;
@@ -5086,32 +5161,39 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
-public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage {
+public class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage {
 	public static final field Key Lnet/mamoe/mirai/message/data/Voice$Key;
 	public static final field SERIAL_NAME Ljava/lang/String;
+	public synthetic fun <init> (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
 	public synthetic fun <init> (Ljava/lang/String;[BJILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public fun contentToString ()Ljava/lang/String;
 	public fun equals (Ljava/lang/Object;)Z
-	public final fun getCodec ()I
+	public static final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
 	public fun getFileName ()Ljava/lang/String;
 	public fun getFileSize ()J
 	public fun getMd5 ()[B
-	public final fun getUrl ()Ljava/lang/String;
+	public fun getUrl ()Ljava/lang/String;
+	public final fun get_codec ()I
 	public fun hashCode ()I
+	public final fun toAudio ()Lnet/mamoe/mirai/message/data/Audio;
 	public fun toString ()Ljava/lang/String;
 }
 
-public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
-	public final fun serializer ()Lkotlinx/serialization/KSerializer;
-}
-
-public final class net/mamoe/mirai/message/data/Voice$Serializer : kotlinx/serialization/KSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$Serializer;
+public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer;
+	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
 	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/Voice;)V
+	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
 public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder {
@@ -5425,14 +5507,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io
 	public static fun uploadAsImage (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image;
 	public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	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 uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;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 static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage;
@@ -5500,14 +5576,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion {
 	public static synthetic fun uploadAsImage$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 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 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 (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)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 uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;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 final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage;
@@ -5581,15 +5651,20 @@ public abstract interface class net/mamoe/mirai/utils/MiraiLogger {
 	public abstract fun error (Ljava/lang/String;)V
 	public abstract fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun error (Ljava/lang/Throwable;)V
-	public abstract fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
 	public abstract fun getIdentity ()Ljava/lang/String;
 	public abstract fun info (Ljava/lang/String;)V
 	public abstract fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun info (Ljava/lang/Throwable;)V
+	public fun isDebugEnabled ()Z
 	public abstract fun isEnabled ()Z
-	public abstract fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun isErrorEnabled ()Z
+	public fun isInfoEnabled ()Z
+	public fun isVerboseEnabled ()Z
+	public fun isWarningEnabled ()Z
+	public fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
 	public static fun setDefaultLoggerCreator (Lkotlin/jvm/functions/Function1;)V
-	public abstract fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
+	public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
 	public abstract fun verbose (Ljava/lang/String;)V
 	public abstract fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun verbose (Ljava/lang/Throwable;)V
@@ -5604,6 +5679,23 @@ public final class net/mamoe/mirai/utils/MiraiLogger$Companion {
 	public final fun setDefaultLoggerCreator (Lkotlin/jvm/functions/Function1;)V
 }
 
+public abstract interface class net/mamoe/mirai/utils/MiraiLogger$Factory {
+	public static final field INSTANCE Lnet/mamoe/mirai/utils/MiraiLogger$Factory$INSTANCE;
+	public fun create (Ljava/lang/Class;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public abstract fun create (Ljava/lang/Class;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public static synthetic fun create$default (Lnet/mamoe/mirai/utils/MiraiLogger$Factory;Ljava/lang/Class;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public static synthetic fun create$default (Lnet/mamoe/mirai/utils/MiraiLogger$Factory;Lkotlin/reflect/KClass;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/MiraiLogger;
+}
+
+public final class net/mamoe/mirai/utils/MiraiLogger$Factory$INSTANCE : net/mamoe/mirai/utils/MiraiLogger$Factory {
+	public fun create (Ljava/lang/Class;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Ljava/lang/Class;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+}
+
 public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/mirai/utils/MiraiLogger {
 	public fun <init> ()V
 	public final fun debug (Ljava/lang/String;)V
@@ -5869,7 +5961,12 @@ public final class net/mamoe/mirai/utils/SingleFileLogger : net/mamoe/mirai/util
 	public fun info (Ljava/lang/String;)V
 	public fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun info (Ljava/lang/Throwable;)V
+	public fun isDebugEnabled ()Z
 	public fun isEnabled ()Z
+	public fun isErrorEnabled ()Z
+	public fun isInfoEnabled ()Z
+	public fun isVerboseEnabled ()Z
+	public fun isWarningEnabled ()Z
 	public fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
 	public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
 	public fun verbose (Ljava/lang/String;)V

+ 123 - 31
binary-compatibility-validator/api/binary-compatibility-validator.api

@@ -176,6 +176,11 @@ public abstract interface class net/mamoe/mirai/contact/AnonymousMember : net/ma
 	public fun uploadImage (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
 
+public abstract interface class net/mamoe/mirai/contact/AudioSupported : net/mamoe/mirai/contact/Contact {
+	public fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public abstract fun uploadAudio (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
 public final class net/mamoe/mirai/contact/BotIsBeingMutedException : java/lang/RuntimeException {
 	public fun <init> (Lnet/mamoe/mirai/contact/Group;)V
 	public final fun getTarget ()Lnet/mamoe/mirai/contact/Group;
@@ -326,7 +331,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
 	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, net/mamoe/mirai/contact/VoiceSupported {
+public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User {
 	public fun delete ()V
 	public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
@@ -338,7 +343,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, net/mamoe/mirai/contact/FileSupported, net/mamoe/mirai/contact/VoiceSupported {
+public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, 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
@@ -365,6 +370,8 @@ public abstract interface class net/mamoe/mirai/contact/Group : kotlinx/coroutin
 	public fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;)Z
 	public abstract fun setEssenceMessage (Lnet/mamoe/mirai/message/data/MessageSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public abstract fun setName (Ljava/lang/String;)V
+	public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice;
+	public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 }
 
 public final class net/mamoe/mirai/contact/Group$Companion {
@@ -446,6 +453,8 @@ public abstract interface class net/mamoe/mirai/contact/NormalMember : net/mamoe
 	public fun isMuted ()Z
 	public fun kick (Ljava/lang/String;)V
 	public abstract fun kick (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+	public fun kick (Ljava/lang/String;Z)V
+	public abstract fun kick (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun modifyAdmin (Z)V
 	public abstract fun modifyAdmin (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public fun nudge ()Lnet/mamoe/mirai/message/action/MemberNudge;
@@ -558,11 +567,6 @@ public abstract interface class net/mamoe/mirai/contact/UserOrBot : net/mamoe/mi
 	public abstract fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
 }
 
-public abstract interface class net/mamoe/mirai/contact/VoiceSupported : net/mamoe/mirai/contact/Contact {
-	public fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;)Lnet/mamoe/mirai/message/data/Voice;
-	public abstract fun uploadVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-}
-
 public abstract interface class net/mamoe/mirai/contact/announcement/Announcement {
 	public static final field Companion Lnet/mamoe/mirai/contact/announcement/Announcement$Companion;
 	public abstract fun getContent ()Ljava/lang/String;
@@ -1754,6 +1758,8 @@ public final class net/mamoe/mirai/event/SubscribeMessagesKt {
 	public static synthetic fun subscribeStrangerMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
 	public static final fun subscribeTempMessages (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
 	public static synthetic fun subscribeTempMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
+	public static final fun subscribeUserMessages (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
+	public static synthetic fun subscribeUserMessages$default (Lnet/mamoe/mirai/event/EventChannel;Lkotlin/coroutines/CoroutineContext;Lnet/mamoe/mirai/event/ConcurrencyKind;Lnet/mamoe/mirai/event/EventPriority;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;
 }
 
 public final class net/mamoe/mirai/event/SyncFromEventKt {
@@ -3154,6 +3160,41 @@ public final class net/mamoe/mirai/message/data/AtAll : net/mamoe/mirai/message/
 	public fun toString ()Ljava/lang/String;
 }
 
+public abstract interface class net/mamoe/mirai/message/data/Audio : net/mamoe/mirai/message/data/MessageContent {
+	public static final field Key Lnet/mamoe/mirai/message/data/Audio$Key;
+	public fun contentToString ()Ljava/lang/String;
+	public abstract fun getCodec ()Lnet/mamoe/mirai/message/data/AudioCodec;
+	public abstract fun getExtraData ()[B
+	public abstract fun getFileMd5 ()[B
+	public abstract fun getFileSize ()J
+	public abstract fun getFilename ()Ljava/lang/String;
+	public abstract fun toString ()Ljava/lang/String;
+}
+
+public final class net/mamoe/mirai/message/data/Audio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+}
+
+public final class net/mamoe/mirai/message/data/AudioCodec : java/lang/Enum {
+	public static final field AMR Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final field Companion Lnet/mamoe/mirai/message/data/AudioCodec$Companion;
+	public static final field SILK Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun getFormatName ()Ljava/lang/String;
+	public final fun getId ()I
+	public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public static fun values ()[Lnet/mamoe/mirai/message/data/AudioCodec;
+}
+
+public final class net/mamoe/mirai/message/data/AudioCodec$Companion {
+	public final fun fromFormatName (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromFormatNameOrNull (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromId (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+	public final fun fromIdOrNull (I)Lnet/mamoe/mirai/message/data/AudioCodec;
+}
+
 public abstract interface class net/mamoe/mirai/message/data/ConstrainSingle : net/mamoe/mirai/message/data/SingleMessage {
 	public abstract fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
 }
@@ -4495,6 +4536,8 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun At (Lnet/mamoe/mirai/contact/UserOrBot;)Lnet/mamoe/mirai/message/data/At;
 	public static final synthetic fun FileMessage (Ljava/lang/String;ILjava/lang/String;J)Lnet/mamoe/mirai/message/data/FileMessage;
 	public static final synthetic fun Image (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/Image;
+	public static final synthetic fun OfflineAudio (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public static final synthetic fun OfflineAudio (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
 	public static final synthetic fun UnsupportedMessage ([B)Lnet/mamoe/mirai/message/data/UnsupportedMessage;
 	public static final synthetic fun at (Lnet/mamoe/mirai/contact/Member;)Lnet/mamoe/mirai/message/data/At;
 	public static final fun buildMessageChain (ILkotlin/jvm/functions/Function1;)Lnet/mamoe/mirai/message/data/MessageChain;
@@ -4537,6 +4580,7 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
 	public static final synthetic fun toMessageChain ([Lnet/mamoe/mirai/message/data/Message;)Lnet/mamoe/mirai/message/data/MessageChain;
 	public static final fun toOfflineMessageSource (Lnet/mamoe/mirai/message/data/OnlineMessageSource;)Lnet/mamoe/mirai/message/data/OfflineMessageSource;
 	public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText;
+	public static final synthetic fun toVoice (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
 }
 
 public final class net/mamoe/mirai/message/data/MusicKind : java/lang/Enum {
@@ -4600,6 +4644,26 @@ public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
+public abstract interface class net/mamoe/mirai/message/data/OfflineAudio : net/mamoe/mirai/message/data/Audio {
+	public static final field Key Lnet/mamoe/mirai/message/data/OfflineAudio$Key;
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
+public abstract interface class net/mamoe/mirai/message/data/OfflineAudio$Factory {
+	public static final field INSTANCE Lnet/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE;
+	public abstract fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+}
+
+public final class net/mamoe/mirai/message/data/OfflineAudio$Factory$INSTANCE : net/mamoe/mirai/message/data/OfflineAudio$Factory {
+	public fun create (Ljava/lang/String;[BJLnet/mamoe/mirai/message/data/AudioCodec;[B)Lnet/mamoe/mirai/message/data/OfflineAudio;
+	public fun from (Lnet/mamoe/mirai/message/data/OnlineAudio;)Lnet/mamoe/mirai/message/data/OfflineAudio;
+}
+
+public final class net/mamoe/mirai/message/data/OfflineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
 public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/mamoe/mirai/message/data/MessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OfflineMessageSource$Key;
 	public fun <init> ()V
@@ -4609,6 +4673,17 @@ public abstract class net/mamoe/mirai/message/data/OfflineMessageSource : net/ma
 public final class net/mamoe/mirai/message/data/OfflineMessageSource$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
 }
 
+public abstract interface class net/mamoe/mirai/message/data/OnlineAudio : net/mamoe/mirai/message/data/Audio {
+	public static final field Key Lnet/mamoe/mirai/message/data/OnlineAudio$Key;
+	public static final field SERIAL_NAME Ljava/lang/String;
+	public abstract fun getLength ()J
+	public abstract fun getUrlForDownload ()Ljava/lang/String;
+}
+
+public final class net/mamoe/mirai/message/data/OnlineAudio$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public static final field SERIAL_NAME Ljava/lang/String;
+}
+
 public abstract class net/mamoe/mirai/message/data/OnlineMessageSource : net/mamoe/mirai/message/data/MessageSource {
 	public static final field Key Lnet/mamoe/mirai/message/data/OnlineMessageSource$Key;
 	public abstract fun getBot ()Lnet/mamoe/mirai/Bot;
@@ -5086,32 +5161,39 @@ public final class net/mamoe/mirai/message/data/VipFace$Kind$Companion {
 	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
-public final class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage {
+public class net/mamoe/mirai/message/data/Voice : net/mamoe/mirai/message/data/PttMessage {
 	public static final field Key Lnet/mamoe/mirai/message/data/Voice$Key;
 	public static final field SERIAL_NAME Ljava/lang/String;
+	public synthetic fun <init> (ILjava/lang/String;[BJILjava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
 	public synthetic fun <init> (Ljava/lang/String;[BJILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public fun contentToString ()Ljava/lang/String;
 	public fun equals (Ljava/lang/Object;)Z
-	public final fun getCodec ()I
+	public static final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
 	public fun getFileName ()Ljava/lang/String;
 	public fun getFileSize ()J
 	public fun getMd5 ()[B
-	public final fun getUrl ()Ljava/lang/String;
+	public fun getUrl ()Ljava/lang/String;
+	public final fun get_codec ()I
 	public fun hashCode ()I
+	public final fun toAudio ()Lnet/mamoe/mirai/message/data/Audio;
 	public fun toString ()Ljava/lang/String;
 }
 
-public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
-	public final fun serializer ()Lkotlinx/serialization/KSerializer;
-}
-
-public final class net/mamoe/mirai/message/data/Voice$Serializer : kotlinx/serialization/KSerializer {
-	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$Serializer;
+public final class net/mamoe/mirai/message/data/Voice$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
+	public static final field INSTANCE Lnet/mamoe/mirai/message/data/Voice$$serializer;
+	public static final synthetic field descriptor Lkotlinx/serialization/descriptors/SerialDescriptor;
+	public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
 	public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
 	public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lnet/mamoe/mirai/message/data/Voice;
 	public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
 	public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
 	public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lnet/mamoe/mirai/message/data/Voice;)V
+	public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
+}
+
+public final class net/mamoe/mirai/message/data/Voice$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
+	public final fun fromAudio (Lnet/mamoe/mirai/message/data/Audio;)Lnet/mamoe/mirai/message/data/Voice;
+	public final fun serializer ()Lkotlinx/serialization/KSerializer;
 }
 
 public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder {
@@ -5425,14 +5507,8 @@ public abstract interface class net/mamoe/mirai/utils/ExternalResource : java/io
 	public static fun uploadAsImage (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;)Lnet/mamoe/mirai/message/data/Image;
 	public static fun uploadAsImage (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/Contact;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
 	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 uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public static fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;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 static fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage;
@@ -5500,14 +5576,8 @@ public final class net/mamoe/mirai/utils/ExternalResource$Companion {
 	public static synthetic fun uploadAsImage$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 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 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 (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Ljava/io/File;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-	public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Ljava/io/InputStream;Lnet/mamoe/mirai/contact/VoiceSupported;Lkotlin/coroutines/Continuation;)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 uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;)Lnet/mamoe/mirai/message/data/Voice;
-	public final fun uploadAsVoice (Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/contact/VoiceSupported;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 final fun uploadTo (Ljava/io/File;Lnet/mamoe/mirai/contact/FileSupported;Ljava/lang/String;Lnet/mamoe/mirai/utils/RemoteFile$ProgressionCallback;)Lnet/mamoe/mirai/message/data/FileMessage;
@@ -5581,15 +5651,20 @@ public abstract interface class net/mamoe/mirai/utils/MiraiLogger {
 	public abstract fun error (Ljava/lang/String;)V
 	public abstract fun error (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun error (Ljava/lang/Throwable;)V
-	public abstract fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun getFollower ()Lnet/mamoe/mirai/utils/MiraiLogger;
 	public abstract fun getIdentity ()Ljava/lang/String;
 	public abstract fun info (Ljava/lang/String;)V
 	public abstract fun info (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun info (Ljava/lang/Throwable;)V
+	public fun isDebugEnabled ()Z
 	public abstract fun isEnabled ()Z
-	public abstract fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun isErrorEnabled ()Z
+	public fun isInfoEnabled ()Z
+	public fun isVerboseEnabled ()Z
+	public fun isWarningEnabled ()Z
+	public fun plus (Lnet/mamoe/mirai/utils/MiraiLogger;)Lnet/mamoe/mirai/utils/MiraiLogger;
 	public static fun setDefaultLoggerCreator (Lkotlin/jvm/functions/Function1;)V
-	public abstract fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
+	public fun setFollower (Lnet/mamoe/mirai/utils/MiraiLogger;)V
 	public abstract fun verbose (Ljava/lang/String;)V
 	public abstract fun verbose (Ljava/lang/String;Ljava/lang/Throwable;)V
 	public fun verbose (Ljava/lang/Throwable;)V
@@ -5604,6 +5679,23 @@ public final class net/mamoe/mirai/utils/MiraiLogger$Companion {
 	public final fun setDefaultLoggerCreator (Lkotlin/jvm/functions/Function1;)V
 }
 
+public abstract interface class net/mamoe/mirai/utils/MiraiLogger$Factory {
+	public static final field INSTANCE Lnet/mamoe/mirai/utils/MiraiLogger$Factory$INSTANCE;
+	public fun create (Ljava/lang/Class;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public abstract fun create (Ljava/lang/Class;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public static synthetic fun create$default (Lnet/mamoe/mirai/utils/MiraiLogger$Factory;Ljava/lang/Class;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public static synthetic fun create$default (Lnet/mamoe/mirai/utils/MiraiLogger$Factory;Lkotlin/reflect/KClass;Ljava/lang/String;ILjava/lang/Object;)Lnet/mamoe/mirai/utils/MiraiLogger;
+}
+
+public final class net/mamoe/mirai/utils/MiraiLogger$Factory$INSTANCE : net/mamoe/mirai/utils/MiraiLogger$Factory {
+	public fun create (Ljava/lang/Class;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Ljava/lang/Class;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;)Lnet/mamoe/mirai/utils/MiraiLogger;
+	public fun create (Lkotlin/reflect/KClass;Ljava/lang/String;)Lnet/mamoe/mirai/utils/MiraiLogger;
+}
+
 public abstract class net/mamoe/mirai/utils/MiraiLoggerPlatformBase : net/mamoe/mirai/utils/MiraiLogger {
 	public fun <init> ()V
 	public final fun debug (Ljava/lang/String;)V

+ 11 - 3
buildSrc/src/main/kotlin/Versions.kt

@@ -39,8 +39,9 @@ object Versions {
 
     const val shadow = "6.1.0"
 
-    const val slf4j = "1.7.30"
-    const val log4j = "2.13.3"
+    const val logback = "1.2.5"
+    const val slf4j = "1.7.32"
+    const val log4j = "2.14.1"
     const val asm = "9.1"
     const val difflib = "1.3.0"
     const val netty = "4.1.63.Final"
@@ -82,12 +83,19 @@ val `ktor-client-core` = ktor("client-core", Versions.ktor)
 val `ktor-client-cio` = ktor("client-cio", Versions.ktor)
 val `ktor-client-okhttp` = ktor("client-okhttp", Versions.ktor)
 val `ktor-client-android` = ktor("client-android", Versions.ktor)
+val `ktor-client-logging` = ktor("client-logging", Versions.ktor)
 val `ktor-network` = ktor("network", Versions.ktor)
 val `ktor-client-serialization-jvm` = ktor("client-serialization-jvm", Versions.ktor)
 
-const val slf4j = "org.slf4j:slf4j-api:" + Versions.slf4j
+const val `logback-classic` = "ch.qos.logback:logback-classic:" + Versions.logback
+
+const val `slf4j-api` = "org.slf4j:slf4j-api:" + Versions.slf4j
 const val `slf4j-simple` = "org.slf4j:slf4j-simple:" + Versions.slf4j
+
 const val `log4j-api` = "org.apache.logging.log4j:log4j-api:" + Versions.log4j
+const val `log4j-core` = "org.apache.logging.log4j:log4j-core:" + Versions.log4j
+const val `log4j-slf4j-impl` = "org.apache.logging.log4j:log4j-slf4j-impl:" + Versions.log4j
+const val `log4j-to-slf4j` = "org.apache.logging.log4j:log4j-to-slf4j:" + Versions.log4j
 
 val ATTRIBUTE_MIRAI_TARGET_PLATFORM: Attribute<String> = Attribute.of("mirai.target.platform", String::class.java)
 

+ 148 - 9
docs/Bots.md

@@ -114,6 +114,8 @@ loginSolver = YourLoginSolver
 setLoginSolver(new YourLoginSolver())
 ```
 
+> 要获取更多有关 `LoginSolver` 的信息,查看 [LoginSolver.kt](../mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt#L32)
+
 ### 常用配置
 
 #### 修改运行目录
@@ -173,6 +175,11 @@ setDeviceInfo(bot -> /* create device info */)
 
 在线生成自定义设备信息的 `device.json`: https://ryoii.github.io/mirai-devicejs-generator/
 
+#### 使用其他日志库接管 mirai 日志系统
+*mirai 2.7 起支持*
+
+使用 Log4J, SLF4J 等接管 mirai 日志系统后则可使用它们的过滤等高级功能。参阅 [mirai-logging](../logging/README.md) 以获取更多信息。
+
 #### 重定向日志
 Bot 有两个日志类别,`Bot` 或 `Net`。`Bot` 为通常日志,如收到事件。`Net` 为网络日志,包含收到和发出的每一个包和网络层解析时遇到的错误。
 
@@ -185,7 +192,13 @@ redirectNetworkLogToFile()
 redirectNetworkLogToDirectory()
 ```
 
-手动覆盖日志:
+关闭日志(将会完全禁用日志功能, 无论是否已经通过第三方日志库接管日志系统):
+```
+noNetworkLog()
+noBotLog()
+```
+
+手动覆盖日志(不建议[(?)](../logging/README.md)):
 ```
 // Kotlin
 networkLoggerSupplier = { bot -> /* create logger */ }
@@ -196,14 +209,6 @@ setNetworkLoggerSupplier(bot -> /* create logger */)
 setBotLoggerSupplier(bot -> /* create logger */)
 ```
 
-关闭日志:
-```
-noNetworkLog()
-noBotLog()
-```
-
-> 要获取更多有关 `LoginSolver` 的信息,查看 [LoginSolver.kt](../mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt#L32)
-
 #### 启用列表缓存
 Mirai 在启动时会拉取全部好友列表和群成员列表。当账号拥有过多群时登录可能缓慢,开启列表缓存会大幅加速登录过程。
 
@@ -271,3 +276,137 @@ contactListCache.setSaveIntervalMillis(60000) // 可选设置有更新时的保
 > 下一步,[Contacts](Contacts.md)
 >
 > [回到 Mirai 文档索引](CoreAPI.md)
+
+
+<!--
+
+## 附录
+
+[Ktor]: https://github.com/ktor.io/ktor
+
+### 使用 Log4J2 接管 mirai 日志系统
+
+添加依赖:
+
+|         Group ID         |   Artifact ID    | 最低版本号 | 说明                                          |
+|:------------------------:|:----------------:|:--------:|:---------------------------------------------|
+| org.apache.logging.log4j |    log4j-api     |  2.14.1  | Log4J2 API 模块, 将会覆盖 mirai 依赖的 log4j-api |
+| org.apache.logging.log4j |    log4j-core    |  2.14.1  | Log4J2 核心实现模块, 所有相关模块版本必须相同        |
+| org.apache.logging.log4j | log4j-slf4j-impl |  2.14.1  | 用于将对 SLF4J 的调用转换为对 Log4J 的调用         |
+
+要添加 `log4j-slf4j-impl` 是因为 [Ktor] 使用了 SLF4J. 添加该模块可以让整个应用统一使用同一个日志库.
+
+#### Gradle 示例
+
+*`build.gradle.kts`*
+```kotlin
+dependencies {
+    val log4jVersion = "2.14.1"
+    implementation("org.apache.logging.log4j:log4j-api:$log4jVersion")
+    implementation("org.apache.logging.log4j:log4j-core:$log4jVersion")
+    implementation("org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion")
+
+    implementation("net.mamoe:mirai-core:2.7-RC") // 示例版本号
+}
+```
+
+#### Maven 示例
+
+*`pom.xml`*
+```xml
+<dependencies>
+    <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api</artifactId>
+        <version>2.14.1</version>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-core</artifactId>
+        <version>2.14.1</version>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-slf4j-impl</artifactId>
+        <version>2.14.1</version>
+    </dependency>
+    <dependency>
+        <groupId>net.mamoe</groupId>
+        <artifactId>mirai-core</artifactId>
+        <version>2.7-RC</version>
+    </dependency>
+</dependencies>
+```
+
+### 使用 SLF4J 接管 mirai 日志系统
+
+添加依赖:
+
+|         Group ID         |  Artifact ID   | 最低版本号 | 说明                                          |
+|:------------------------:|:--------------:|:--------:|:---------------------------------------------|
+| org.apache.logging.log4j |   log4j-api    |  2.14.1  | Log4J2 API 模块, 将会覆盖 mirai 依赖的 log4j-api |
+| org.apache.logging.log4j | log4j-to-slf4j |  2.14.1  | 用于将对 Log4J 的调用转换为对 SLF4J 的调用         |
+|        org.slf4j         |   slf4j-api    |  1.7.32  | SLF4J API 模块, 将会覆盖 [Ktor] 依赖的 slf4j-api |
+
+除上以外, 还要添加一个 SLF4J 的实现, 例如 `slf4j-simple` 或 `logback-classic`.
+这与通常的操作方式相同.
+
+#### Gradle 示例
+
+*`build.gradle.kts`*
+```kotlin
+dependencies {
+    implementation("org.apache.logging.log4j:log4j-api:2.14.1")
+    implementation("org.apache.logging.log4j:log4j-to-slf4j:2.14.1")
+    implementation("org.slf4j:slf4j-api:1.7.32")
+
+    implementation("org.slf4j:slf4j-simple:1.7.32") // 若要使用 slf4j-simple
+    implementation("ch.qos.logback:logback-classic:1.2.5") // 若要使用 logback
+
+
+    implementation("net.mamoe:mirai-core:2.7-RC") // 示例版本号
+}
+```
+
+#### Maven 示例
+
+*`pom.xml`*
+```xml
+<dependencies>
+    <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-api</artifactId>
+        <version>2.14.1</version>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-to-slf4j</artifactId>
+        <version>2.14.1</version>
+    </dependency>
+    <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-api</artifactId>
+        <version>1.7.32</version>
+    </dependency>
+
+    <dependency> <!-- 若要使用 slf4j-simple --
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-simple</artifactId>
+        <version>1.7.32</version>
+    </dependency>
+
+    <dependency> <!-- 若要使用 logback --
+        <groupId>ch.qos.logback</groupId>
+        <artifactId>logback-classic</artifactId>
+        <version>1.2.5</version>
+    </dependency>
+
+    <dependency>
+        <groupId>net.mamoe</groupId>
+        <artifactId>mirai-core</artifactId>
+        <version>2.7-RC</version> <!--示例版本号--
+    </dependency>
+</dependencies>
+```
+
+-->

+ 77 - 0
logging/README.md

@@ -0,0 +1,77 @@
+# mirai-logging
+
+Mirai 日志转接模块,用于使用各大主流日志库接管 Mirai 日志系统以实现统一管理。
+
+请注意,该接管不会影响通过 `redirectBotLogToFile()` 将 Bot 日志重定向到文件以及其他依赖于 mirai 内建日志系统的功能。在接管日志后,需要在目标日志库以配置等方式实现重定向等功能。
+
+仅在 mirai 2.7 起提供。
+
+## 模块列表
+
+[Log4j2]: https://logging.apache.org/log4j/
+[SLF4J]: http://www.slf4j.org/
+[Logback]: http://logback.qos.ch/
+
+若使用 SLF4J 的转接器,则会同时将对 Log4J2 的调用转向 SLF4J。同样地,若使用 Log4J2 的转接器,同时将对 SLF4J 的调用转向 Log4J2。因此可使用单一日志库。
+
+|   groupId   |          artifactId           | 对接日志库  | 备注                            | 最低 mirai 版本 |
+|:-----------:|:-----------------------------:|:---------:|:-------------------------------|:--------------:|
+| `net.mamoe` |    `mirai-logging-log4j2`     | [Log4J2]  | 使用 log4j2-core.               |     2.7.0      |
+| `net.mamoe` | `mirai-logging-slf4j-logback` | [Logback] | 使用 logback-classic.           |     2.7.0      |
+| `net.mamoe` | `mirai-logging-slf4j-simple`  |  [SLF4J]  | 使用 slf4j-simple.              |     2.7.0      |
+| `net.mamoe` |     `mirai-logging-slf4j`     |  [SLF4J]  | 需要自定义添加 SLF4J 的任意实现模块. |     2.7.0     |
+
+## 使用方法
+
+选择上述模块中的其中一个,在运行时 classpath 包含即可。
+
+若使用构建工具,可将其作为类似 mirai-core 的依赖添加,版本号与 mirai-core 相同。
+
+### Gradle Kotlin DSL
+
+在 `build.gradle.kts` 添加:
+
+```kotlin
+dependencies {
+    api("net.mamoe", "mirai-core", "2.6.7")
+    api("net.mamoe", "mirai-logging-log4j2", "2.6.7") // 在依赖 mirai-core 或 mirai-core-api 的前提下额外添加日志转接模块. 版本号相同
+}
+```
+
+### Gradle Groovy DSL
+
+在 `build.gradle.kts` 添加:
+
+```groovy
+dependencies {
+    api 'net.mamoe:mirai-core:2.6.7'
+    api 'net.mamoe:mirai-logging-log4j2:2.6.7' // 在依赖 mirai-core 或 mirai-core-api 的前提下额外添加日志转接模块. 版本号相同
+}
+```
+
+### Maven
+
+在 `pom.xml` 添加:
+
+```xml
+<dependencies>
+    <dependency>
+        <groupId>net.mamoe</groupId>
+        <artifactId>mirai-core-jvm</artifactId>
+        <version>2.6.7</version>
+    </dependency>
+    
+    <!--在依赖 mirai-core 或 mirai-core-api 的前提下额外添加日志转接模块. 版本号相同-->
+    <dependency>
+        <groupId>net.mamoe</groupId>
+        <artifactId>mirai-logging-log4j2</artifactId>
+        <version>2.6.7</version>
+    </dependency>
+</dependencies>
+```
+
+## 自行实现日志转接
+
+Mirai 通过 Java `ServiceLoader` 加载 `MiraiLogger.Factory`。只需要实现该类型并以标准 service 方式提供即可(如 `resources` 中 `META-INF/services`)。
+
+但**更推荐**使用上述转接模块先转接到 Log4J2 或 SLF4J,然后再基于 log4j-api 或 slf4j-api 转接。这样更稳定,也可以获取到 `Marker` 的支持。

+ 37 - 0
logging/mirai-logging-log4j2/build.gradle.kts

@@ -0,0 +1,37 @@
+/*
+ * 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("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    id("java")
+    `maven-publish`
+}
+
+version = Versions.core
+description = "Mirai Log4J Adapter"
+
+kotlin {
+    explicitApi()
+}
+
+dependencies {
+    api(project(":mirai-core-api"))
+    api(`log4j-api`)
+    api(`log4j-core`)
+    api(`log4j-slf4j-impl`)
+
+
+    testImplementation(`slf4j-api`)
+    testImplementation(project(":mirai-core"))
+    testImplementation(project(":mirai-core-utils"))
+}
+
+configurePublishing("mirai-logging-log4j2")

+ 10 - 0
logging/mirai-logging-log4j2/resources/META-INF/services/net.mamoe.mirai.utils.MiraiLogger$Factory

@@ -0,0 +1,10 @@
+#
+# Copyright 2019-2021 Mamoe Technologies and contributors.
+#
+# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+#
+# https://github.com/mamoe/mirai/blob/dev/LICENSE
+#
+
+net.mamoe.mirai.utils.logging.MiraiLog4JFactory

+ 30 - 0
logging/mirai-logging-log4j2/src/MiraiLog4JFactory.kt

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils.logging
+
+import net.mamoe.mirai.utils.MiraiInternalApi
+import net.mamoe.mirai.utils.MiraiLogger
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.MarkerManager
+
+/**
+ * 使用 Log4J 接管 mirai 日志系统.
+ */
+@MiraiInternalApi
+public class MiraiLog4JFactory : MiraiLogger.Factory {
+    override fun create(requester: Class<*>, identity: String?): MiraiLogger {
+        val logger = LogManager.getLogger(requester)
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        return net.mamoe.mirai.internal.utils.Log4jLoggerAdapter(
+            logger,
+            MarkerManager.getMarker(identity ?: logger.name).addParents(net.mamoe.mirai.internal.utils.MARKER_MIRAI)
+        )
+    }
+}

+ 63 - 0
logging/mirai-logging-log4j2/test/MiraiLog4JAdapterTest.kt

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils.logging
+
+import io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.loadService
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+
+internal class MiraiLog4JAdapterTest {
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun `services prevail than legacy overrides`() {
+        MiraiLogger.setDefaultLoggerCreator {
+            net.mamoe.mirai.utils.SimpleLogger("my logger") { _: String?, _: Throwable? -> }
+        }
+
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        assertIs<net.mamoe.mirai.internal.utils.Log4jLoggerAdapter>(MiraiLogger.Factory.create(this::class))
+    }
+
+    @Test
+    fun `using log4j`() {
+        assertIs<MiraiLog4JFactory>(loadService(MiraiLogger.Factory::class))
+        val logger = MiraiLogger.Factory.create(this::class)
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        assertIs<net.mamoe.mirai.internal.utils.Log4jLoggerAdapter>(logger)
+    }
+
+    @Test
+    fun `print test`() {
+        val out = ByteArrayOutputStream()
+        System.setOut(PrintStream(out, true))
+        System.setErr(PrintStream(out, true))
+        HttpClient(OkHttp)
+        val logger = MiraiLogger.Factory.create(this::class)
+        logger.error("Hi")
+        /*
+        SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
+SLF4J: Defaulting to no-operation (NOP) logger implementation
+SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
+ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
+
+         */
+        out.flush()
+        println(out.toString())
+        assertFalse { out.toString().contains("slf4j", ignoreCase = true) }
+        assertFalse { out.toString().contains("Log4j2 could not find a logging implementation", ignoreCase = true) }
+    }
+}

+ 38 - 0
logging/mirai-logging-slf4j-logback/build.gradle.kts

@@ -0,0 +1,38 @@
+/*
+ * 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("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    id("java")
+    `maven-publish`
+}
+
+version = Versions.core
+description = "Mirai SLF4J Adapter with logback-classic"
+
+kotlin {
+    explicitApi()
+}
+
+dependencies {
+    api(project(":mirai-core-api"))
+    api(project(":mirai-logging-slf4j")) // mirai -> slf4j
+    implementation(project(":mirai-logging-log4j2")) {
+        exclude("org.apache.logging.log4j", "log4j-slf4j-impl")
+    } // mirai -> log4j2
+    api(`slf4j-api`)
+    api(`logback-classic`)
+
+    testImplementation(project(":mirai-core"))
+    testImplementation(project(":mirai-core-utils"))
+}
+
+configurePublishing("mirai-logging-slf4j-logback")

+ 63 - 0
logging/mirai-logging-slf4j-logback/test/MiraiSlf4JLogbackAdapterTest.kt

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils.logging
+
+import io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.loadService
+import org.junit.jupiter.api.Order
+import org.slf4j.LoggerFactory
+import org.slf4j.helpers.NOPLogger
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+import kotlin.test.assertIsNot
+
+internal class MiraiSlf4JLogbackAdapterTest {
+
+    @Order(1)
+    @Test
+    fun `using log4j`() {
+        assertIs<MiraiLog4JFactory>(loadService(MiraiLogger.Factory::class))
+        val logger = MiraiLogger.Factory.create(this::class)
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        assertIs<net.mamoe.mirai.internal.utils.Log4jLoggerAdapter>(logger)
+    }
+
+    @Order(0)
+    @Test
+    fun `print test`() {
+        val out = ByteArrayOutputStream()
+        System.setOut(PrintStream(out, true))
+        System.setErr(PrintStream(out, true))
+
+        assertIsNot<NOPLogger>(LoggerFactory.getLogger("s"))
+        HttpClient(OkHttp)
+
+        val logger = MiraiLogger.Factory.create(this::class)
+        logger.error("Hi")
+        /*
+        SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
+SLF4J: Defaulting to no-operation (NOP) logger implementation
+SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
+ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
+
+         */
+        out.flush()
+        println(out.toString())
+        assertFalse { out.toString().contains("Log4j2 could not find a logging implementation", ignoreCase = true) }
+        assertFalse {
+            out.toString().contains("SLF4J: Defaulting to no-operation (NOP) logger implementation", ignoreCase = true)
+        }
+    }
+}

+ 38 - 0
logging/mirai-logging-slf4j-simple/build.gradle.kts

@@ -0,0 +1,38 @@
+/*
+ * 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("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    id("java")
+    `maven-publish`
+}
+
+version = Versions.core
+description = "Mirai SLF4J Adapter with slf4j-simple"
+
+kotlin {
+    explicitApi()
+}
+
+dependencies {
+    api(project(":mirai-core-api"))
+    api(project(":mirai-logging-slf4j")) // mirai -> slf4j
+    implementation(project(":mirai-logging-log4j2")) {
+        exclude("org.apache.logging.log4j", "log4j-slf4j-impl")
+    } // mirai -> log4j2
+    api(`slf4j-api`)
+    api(`slf4j-simple`)
+
+    testImplementation(project(":mirai-core"))
+    testImplementation(project(":mirai-core-utils"))
+}
+
+configurePublishing("mirai-logging-slf4j-simple")

+ 63 - 0
logging/mirai-logging-slf4j-simple/test/MiraiSlf4JSimpleAdapterTest.kt

@@ -0,0 +1,63 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils.logging
+
+import io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.loadService
+import org.junit.jupiter.api.Order
+import org.slf4j.LoggerFactory
+import org.slf4j.helpers.NOPLogger
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+import kotlin.test.assertIsNot
+
+internal class MiraiSlf4JSimpleAdapterTest {
+
+    @Order(1)
+    @Test
+    fun `using log4j`() {
+        assertIs<MiraiLog4JFactory>(loadService(MiraiLogger.Factory::class))
+        val logger = MiraiLogger.Factory.create(this::class)
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        assertIs<net.mamoe.mirai.internal.utils.Log4jLoggerAdapter>(logger)
+    }
+
+    @Order(0)
+    @Test
+    fun `print test`() {
+        val out = ByteArrayOutputStream()
+        System.setOut(PrintStream(out, true))
+        System.setErr(PrintStream(out, true))
+
+        assertIsNot<NOPLogger>(LoggerFactory.getLogger("s"))
+        HttpClient(OkHttp)
+
+        val logger = MiraiLogger.Factory.create(this::class)
+        logger.error("Hi")
+        /*
+        SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
+SLF4J: Defaulting to no-operation (NOP) logger implementation
+SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
+ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
+
+         */
+        out.flush()
+        println(out.toString())
+        assertFalse { out.toString().contains("Log4j2 could not find a logging implementation", ignoreCase = true) }
+        assertFalse {
+            out.toString().contains("SLF4J: Defaulting to no-operation (NOP) logger implementation", ignoreCase = true)
+        }
+    }
+}

+ 37 - 0
logging/mirai-logging-slf4j/build.gradle.kts

@@ -0,0 +1,37 @@
+/*
+ * 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("UnusedImport")
+
+plugins {
+    kotlin("jvm")
+    id("java")
+    `maven-publish`
+}
+
+version = Versions.core
+description = "Mirai SLF4J Adapter"
+
+kotlin {
+    explicitApi()
+}
+
+dependencies {
+    api(project(":mirai-core-api"))
+    implementation(project(":mirai-logging-log4j2")) {
+        exclude("org.apache.logging.log4j", "log4j-slf4j-impl")
+    } // mirai -> log4j2
+    implementation(`log4j-to-slf4j`) // log4j2 -> slf4j
+    api(`slf4j-api`)
+
+    testImplementation(project(":mirai-core"))
+    testImplementation(project(":mirai-core-utils"))
+}
+
+configurePublishing("mirai-logging-slf4j")

+ 62 - 0
logging/mirai-logging-slf4j/test/MiraiSlf4JAdapterTest.kt

@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils.logging
+
+import io.ktor.client.*
+import io.ktor.client.engine.okhttp.*
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.loadService
+import org.junit.jupiter.api.Order
+import org.slf4j.LoggerFactory
+import org.slf4j.helpers.NOPLogger
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertIs
+
+internal class MiraiSlf4JAdapterTest {
+
+    @Order(1)
+    @Test
+    fun `using log4j`() {
+        assertIs<MiraiLog4JFactory>(loadService(MiraiLogger.Factory::class))
+        val logger = MiraiLogger.Factory.create(this::class)
+        @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+        assertIs<net.mamoe.mirai.internal.utils.Log4jLoggerAdapter>(logger)
+    }
+
+    @Order(0)
+    @Test
+    fun `print test`() {
+        val out = ByteArrayOutputStream()
+        System.setOut(PrintStream(out, true))
+        System.setErr(PrintStream(out, true))
+
+        assertIs<NOPLogger>(LoggerFactory.getLogger("s"))
+        HttpClient(OkHttp)
+
+        val logger = MiraiLogger.Factory.create(this::class)
+        logger.error("Hi")
+        /*
+        SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
+SLF4J: Defaulting to no-operation (NOP) logger implementation
+SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
+ERROR StatusLogger Log4j2 could not find a logging implementation. Please add log4j-core to the classpath. Using SimpleLogger to log to the console...
+
+         */
+        out.flush()
+        println(out.toString())
+        assertFalse { out.toString().contains("Log4j2 could not find a logging implementation", ignoreCase = true) }
+//        assertTrue {
+//            out.toString().contains("SLF4J: Defaulting to no-operation (NOP) logger implementation", ignoreCase = true)
+//        }
+    }
+}

+ 1 - 1
mirai-console

@@ -1 +1 @@
-Subproject commit 6a63f1416febde02dcf14f11960e9bc6b09b2e8f
+Subproject commit f5f3603dcc4d21eee90b406fa3bf9df30069087c

+ 8 - 2
mirai-core-api/build.gradle.kts

@@ -66,8 +66,8 @@ kotlin {
                 api(`ktor-client-core`)
                 api(`ktor-network`)
 
-                compileOnly(`log4j-api`)
-                compileOnly(slf4j)
+                implementation(`log4j-api`)
+                compileOnly(`slf4j-api`)
 
 
                 // they use Kotlin 1.3 so we need to ignore transitive dependencies
@@ -77,6 +77,12 @@ kotlin {
             }
         }
 
+        commonTest {
+            dependencies {
+                runtimeOnly(`log4j-core`)
+            }
+        }
+
         if (isAndroidSDKAvailable) {
             val androidMain by getting {
                 dependsOn(commonMain)

+ 0 - 16
mirai-core-api/src/commonMain/kotlin/LowLevelApiAccessor.kt

@@ -15,9 +15,7 @@ import kotlinx.coroutines.Job
 import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.*
-import net.mamoe.mirai.message.data.Voice
 import net.mamoe.mirai.utils.MiraiExperimentalApi
-import net.mamoe.mirai.utils.MiraiInternalApi
 import net.mamoe.mirai.utils.NotStableForInheritance
 import net.mamoe.mirai.utils.WeakRef
 import kotlin.annotation.AnnotationTarget.*
@@ -219,18 +217,4 @@ public interface LowLevelApiAccessor {
         groupId: Long,
         seconds: Int,
     )
-
-    /**
-     * 序列化 [Voice.pttInternalInstance]
-     */
-    @LowLevelApi
-    @MiraiInternalApi // For Voice serialize
-    public fun serializePttElem(ptt: Any?): String
-
-    /**
-     * 反序列化 [Voice.pttInternalInstance]
-     */
-    @LowLevelApi
-    @MiraiInternalApi // For Voice serialize
-    public fun deserializePttElem(ptt: String): Any?
 }

+ 45 - 0
mirai-core-api/src/commonMain/kotlin/contact/AudioSupported.kt

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+@file:JvmBlockingBridge
+
+package net.mamoe.mirai.contact
+
+import net.mamoe.kjbb.JvmBlockingBridge
+import net.mamoe.mirai.message.data.Audio
+import net.mamoe.mirai.message.data.OfflineAudio
+import net.mamoe.mirai.utils.ExternalResource
+import net.mamoe.mirai.utils.NotStableForInheritance
+import net.mamoe.mirai.utils.OverFileSizeMaxException
+
+/**
+ * 支持发送语音的 [Contact]
+ *
+ * @since 2.7
+ */
+@NotStableForInheritance
+public interface AudioSupported : Contact {
+    /**
+     * 上传一个语音文件以备发送. [resource] 需要调用方[关闭][ExternalResource.close].
+     *
+     * 多次调用 [uploadAudio] 使用同一个 [resource] 时, 将会发生多次上传, 且有可能产生不同的 [OfflineAudio] 对象, 因为服务器不会提供有关文件是否已经存在于服务器的信息.
+     *
+     * 返回的 [OfflineAudio] 支持序列化, 可以保存后在将来使用, 而不需要立即[发送][Contact.sendMessage]. 但不建议保存太久, 无法确定服务器保留一个文件的时间.
+     *
+     * 建议使用同一个 [Contact] 进行 [uploadAudio] 和 [sendMessage]. 目标对象不同时的行为是不确定的.
+     *
+     * 要获取更多语音相关的信息, 参阅 [Audio].
+     *
+     * @throws OverFileSizeMaxException 当语音文件过大而被服务器拒绝上传时. (最大大小约为 1 MB)
+     * **注意**: 由于服务器不一定会检查大小, 该异常就不一定会因大小超过 1MB 而抛出.
+     *
+     * @since 2.7
+     */
+    public suspend fun uploadAudio(resource: ExternalResource): OfflineAudio
+}

+ 1 - 1
mirai-core-api/src/commonMain/kotlin/contact/Friend.kt

@@ -34,7 +34,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
  * @see FriendMessageEvent
  */
 @NotStableForInheritance
-public interface Friend : User, CoroutineScope, VoiceSupported {
+public interface Friend : User, CoroutineScope, AudioSupported {
     /**
      * 向这个对象发送消息.
      *

+ 13 - 1
mirai-core-api/src/commonMain/kotlin/contact/Group.kt

@@ -19,6 +19,7 @@ import net.mamoe.mirai.contact.announcement.Announcements
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.NotStableForInheritance
 
@@ -52,7 +53,7 @@ import net.mamoe.mirai.utils.NotStableForInheritance
  * ##
  */
 @NotStableForInheritance
-public interface Group : Contact, CoroutineScope, FileSupported, VoiceSupported {
+public interface Group : Contact, CoroutineScope, FileSupported, AudioSupported {
     /**
      * 群名称.
      *
@@ -184,6 +185,17 @@ public interface Group : Contact, CoroutineScope, FileSupported, VoiceSupported
         this.sendMessage(message.toPlainText())
 
 
+    /**
+     * 上传一个语音消息以备发送. 该方法已弃用且将在未来版本删除, 请使用 [uploadAudio].
+     */
+    @Suppress("DEPRECATION")
+    @Deprecated(
+        "use uploadAudio",
+        replaceWith = ReplaceWith("uploadAudio(resource)"),
+        level = DeprecationLevel.WARNING
+    )
+    public suspend fun uploadVoice(resource: ExternalResource): Voice
+
     /**
      * 将一条消息设置为群精华消息, 需要管理员或群主权限.
      * 操作成功返回 `true`.

+ 14 - 0
mirai-core-api/src/commonMain/kotlin/contact/NormalMember.kt

@@ -105,12 +105,26 @@ public interface NormalMember : Member {
      *
      * 管理员可踢出成员, 群主可踢出管理员和群员.
      *
+     * @param block 为 `true` 时拉黑成员
+     *
+     * @see MemberLeaveEvent.Kick 成员被踢出事件.
+     * @throws PermissionDeniedException 无权限修改时
+     *
+     */
+    public suspend fun kick(message: String, block: Boolean)
+
+    /**
+     * 踢出该成员, 默认不拉黑
+     *
+     * 管理员可踢出成员, 群主可踢出管理员和群员.
+     *
      * @see MemberLeaveEvent.Kick 成员被踢出事件.
      * @throws PermissionDeniedException 无权限修改时
      *
      */
     public suspend fun kick(message: String)
 
+
     /**
      * 给予或移除群成员的管理员权限。
      *

+ 0 - 39
mirai-core-api/src/commonMain/kotlin/contact/VoiceSupported.kt

@@ -1,39 +0,0 @@
-/*
- * Copyright 2019-2021 Mamoe Technologies and contributors.
- *
- * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
- * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
- *
- * https://github.com/mamoe/mirai/blob/dev/LICENSE
- */
-
-package net.mamoe.mirai.contact
-
-import net.mamoe.kjbb.JvmBlockingBridge
-import net.mamoe.mirai.message.data.Voice
-import net.mamoe.mirai.utils.ExternalResource
-import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice
-import net.mamoe.mirai.utils.NotStableForInheritance
-import net.mamoe.mirai.utils.OverFileSizeMaxException
-
-/**
- * 支持发送语音的 [Contact]
- *
- * @since 2.7
- */
-@NotStableForInheritance
-public interface VoiceSupported : Contact {
-    /**
-     * 上传一个语音消息以备发送.
-     *
-     * - **请手动关闭 [resource]**
-     * - 请使用 amr 或 silk 格式
-     *
-     * @since 2.7
-     * @see ExternalResource.uploadAsVoice
-     * @throws OverFileSizeMaxException 当语音文件过大而被服务器拒绝上传时. (最大大小约为 1 MB)
-     */
-    @JvmBlockingBridge
-    public suspend fun uploadVoice(resource: ExternalResource): Voice
-
-}

+ 1 - 1
mirai-core-api/src/commonMain/kotlin/event/Event.kt

@@ -190,7 +190,7 @@ internal open class _EventBroadcast {
         }
     }
 
-    private val topLevelEventLogger by lazy { MiraiLogger.create("EventPipeline") }
+    private val topLevelEventLogger by lazy { MiraiLogger.Factory.create(Event::class, "EventPipeline") }
 }
 
 /**

+ 24 - 0
mirai-core-api/src/commonMain/kotlin/event/subscribeMessages.kt

@@ -16,6 +16,7 @@ package net.mamoe.mirai.event
 import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.OtherClient
 import net.mamoe.mirai.contact.Stranger
+import net.mamoe.mirai.contact.User
 import net.mamoe.mirai.event.ConcurrencyKind.CONCURRENT
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.data.content
@@ -119,6 +120,29 @@ public fun <R> EventChannel<*>.subscribeFriendMessages(
     return createBuilder(::FriendMessageSubscribersBuilder, coroutineContext, concurrencyKind, priority).run(listeners)
 }
 
+/**
+ * @since 2.7
+ */
+public typealias UserMessageSubscribersBuilder = MessageSubscribersBuilder<UserMessageEvent, Listener<UserMessageEvent>, Unit, Unit>
+
+/**
+ * 通过 DSL 订阅来自所有 [Bot] 的所 [User] 消息事件. DSL 语法查看 [subscribeMessages].
+ *
+ * @see EventChannel.subscribe 事件监听基础
+ * @see EventChannel 事件通道
+ *
+ * @since 2.7
+ */
+public fun <R> EventChannel<*>.subscribeUserMessages(
+    coroutineContext: CoroutineContext = EmptyCoroutineContext,
+    concurrencyKind: ConcurrencyKind = CONCURRENT,
+    priority: EventPriority = EventPriority.MONITOR,
+    listeners: UserMessageSubscribersBuilder.() -> R
+): R {
+    contract { callsInPlace(listeners, InvocationKind.EXACTLY_ONCE) }
+    return createBuilder(::UserMessageSubscribersBuilder, coroutineContext, concurrencyKind, priority).run(listeners)
+}
+
 @Deprecated(
     "mirai 正计划支持其他渠道发起的临时会话, 届时此定义会变动. 请使用 GroupTempMessageSubscribersBuilder",
     ReplaceWith(

+ 44 - 18
mirai-core-api/src/commonMain/kotlin/internal/message/MessageSerializersImpl.kt

@@ -13,14 +13,12 @@ import kotlinx.serialization.KSerializer
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.descriptors.buildClassSerialDescriptor
-import kotlinx.serialization.modules.PolymorphicModuleBuilder
-import kotlinx.serialization.modules.SerializersModule
-import kotlinx.serialization.modules.overwriteWith
-import kotlinx.serialization.modules.polymorphic
+import kotlinx.serialization.modules.*
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.message.MessageSerializers
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.utils.MiraiInternalApi
+import net.mamoe.mirai.utils.lateinitMutableProperty
 import net.mamoe.mirai.utils.map
 import net.mamoe.mirai.utils.takeElementsFrom
 import kotlin.reflect.KClass
@@ -121,6 +119,7 @@ private val builtInSerializersModule by lazy {
             subclass(SimpleServiceMessage::class, SimpleServiceMessage.serializer())
 
             //  subclass(PttMessage::class, PttMessage.serializer())
+            @Suppress("DEPRECATION")
             subclass(Voice::class, Voice.serializer())
 
             // subclass(HummerMessage::class, HummerMessage.serializer())
@@ -188,31 +187,58 @@ private val builtInSerializersModule by lazy {
 // Tests:
 // net.mamoe.mirai.internal.message.data.MessageSerializationTest
 internal object MessageSerializersImpl : MessageSerializers {
-    @Volatile
-    private var serializersModuleField: SerializersModule? = null
+    private var serializersModuleField: SerializersModule by lateinitMutableProperty {
+        builtInSerializersModule
+    }
+
     override val serializersModule: SerializersModule
         get() {
             Mirai // ensure registered, for tests
-            return serializersModuleField ?: builtInSerializersModule
+            return serializersModuleField
         }
 
     @Synchronized
     override fun <M : SingleMessage> registerSerializer(type: KClass<M>, serializer: KSerializer<M>) {
-        serializersModuleField = serializersModule.overwriteWith(SerializersModule {
-            // contextual(type, serializer)
-            for (superclass in type.allSuperclasses) {
-                if (superclass.isFinal) continue
-                if (!superclass.isSubclassOf(SingleMessage::class)) continue
-                @Suppress("UNCHECKED_CAST")
-                polymorphic(superclass as KClass<Any>) {
-                    subclass(type, serializer)
-                }
-            }
-        })
+        serializersModuleField = serializersModule.overwritePolymorphicWith(type, serializer)
     }
 
     @Synchronized
     override fun registerSerializers(serializersModule: SerializersModule) {
         serializersModuleField = serializersModule.overwriteWith(serializersModule)
     }
+}
+
+internal fun <M : Any> SerializersModule.overwritePolymorphicWith(
+    type: KClass<M>,
+    serializer: KSerializer<M>
+): SerializersModule {
+    return overwriteWith(SerializersModule {
+        // contextual(type, serializer)
+        for (superclass in type.allSuperclasses) {
+            if (superclass.isFinal) continue
+            if (!superclass.isSubclassOf(SingleMessage::class)) continue
+            @Suppress("UNCHECKED_CAST")
+            polymorphic(superclass as KClass<Any>) {
+                subclass(type, serializer)
+            }
+        }
+    })
+}
+
+private inline fun <reified M : SingleMessage> SerializersModuleBuilder.hierarchicallyPolymorphic(serializer: KSerializer<M>) =
+    hierarchicallyPolymorphic(M::class, serializer)
+
+private fun <M : SingleMessage> SerializersModuleBuilder.hierarchicallyPolymorphic(
+    type: KClass<M>,
+    serializer: KSerializer<M>
+) {
+    // contextual(type, serializer)
+    for (superclass in type.allSuperclasses) {
+        if (superclass.isFinal) continue
+        if (!superclass.isSubclassOf(SingleMessage::class)) continue
+        @Suppress("UNCHECKED_CAST")
+        polymorphic(superclass as KClass<Any>) {
+            subclass(type, serializer)
+        }
+    }
 }

+ 1 - 1
mirai-core-api/src/commonMain/kotlin/internal/utils/ExternalResourceLeakObserver.kt

@@ -21,7 +21,7 @@ internal object ExternalResourceLeakObserver : Runnable {
     private val queue = ReferenceQueue<Any>()
     private val references = ConcurrentLinkedDeque<ERReference>()
     private val logger by lazy {
-        MiraiLogger.create("ExternalResourceLeakObserver")
+        MiraiLogger.Factory.create(ExternalResourceLeakObserver::class)
     }
 
     internal class ERReference(

+ 67 - 23
mirai-core-api/src/commonMain/kotlin/internal/utils/LoggerAdapterImpls.kt

@@ -10,83 +10,127 @@
 package net.mamoe.mirai.internal.utils
 
 import net.mamoe.mirai.utils.MiraiLoggerPlatformBase
-import org.slf4j.Logger
-import java.util.logging.Level
+import org.apache.logging.log4j.Marker
+import org.apache.logging.log4j.MarkerManager
+import java.util.logging.Level as JulLevel
+import java.util.logging.Logger as JulLogger
 
-internal class Log4jLogger(private val logger: org.apache.logging.log4j.Logger) : MiraiLoggerPlatformBase() {
+internal class Log4jLoggerAdapter(
+    private val logger: org.apache.logging.log4j.Logger,
+    override val marker: Marker?,
+) : MiraiLoggerPlatformBase(), MarkedMiraiLogger {
 
     override fun verbose0(message: String?, e: Throwable?) {
-        logger.trace(message, e)
+        val marker = marker
+        if (marker != null) logger.trace(marker, message, e)
+        else logger.trace(message, e)
     }
 
     override fun debug0(message: String?, e: Throwable?) {
-        logger.debug(message, e)
+        val marker = marker
+        if (marker != null) logger.debug(marker, message, e)
+        else logger.debug(message, e)
     }
 
     override fun info0(message: String?, e: Throwable?) {
-        logger.info(message, e)
+        val marker = marker
+        if (marker != null) logger.info(marker, message, e)
+        else logger.info(message, e)
     }
 
     override fun warning0(message: String?, e: Throwable?) {
-        logger.warn(message, e)
+        val marker = marker
+        if (marker != null) logger.warn(marker, message, e)
+        else logger.warn(message, e)
     }
 
     override fun error0(message: String?, e: Throwable?) {
-        logger.error(message, e)
+        val marker = marker
+        if (marker != null) logger.error(marker, message, e)
+        else logger.error(message, e)
     }
 
-    override val identity: String?
-        get() = logger.name
+    override val isVerboseEnabled: Boolean get() = logger.isTraceEnabled
+    override val isDebugEnabled: Boolean get() = logger.isDebugEnabled
+    override val isInfoEnabled: Boolean get() = logger.isInfoEnabled
+    override val isWarningEnabled: Boolean get() = logger.isWarnEnabled
+    override val isErrorEnabled: Boolean get() = logger.isErrorEnabled
+
+    override val identity: String? get() = logger.name
+
+    override fun subLogger(name: String): MarkedMiraiLogger {
+        return Log4jLoggerAdapter(logger, Marker(name, marker))
+    }
 
 }
 
-internal class Slf4jLogger(private val logger: Logger) : MiraiLoggerPlatformBase() {
+internal val MARKER_MIRAI by lazy { MarkerManager.getMarker("mirai") }
+
+internal class Slf4jLoggerAdapter(private val logger: org.slf4j.Logger, private val marker: org.slf4j.Marker?) :
+    MiraiLoggerPlatformBase() {
     override fun verbose0(message: String?, e: Throwable?) {
-        logger.trace(message, e)
+        if (marker == null) logger.trace(message, e)
+        else logger.trace(marker, message, e)
     }
 
     override fun debug0(message: String?, e: Throwable?) {
-        logger.debug(message, e)
+        if (marker == null) logger.debug(message, e)
+        else logger.debug(marker, message, e)
     }
 
     override fun info0(message: String?, e: Throwable?) {
-        logger.info(message, e)
+        if (marker == null) logger.info(message, e)
+        else logger.info(marker, message, e)
     }
 
     override fun warning0(message: String?, e: Throwable?) {
-        logger.warn(message, e)
+        if (marker == null) logger.warn(message, e)
+        else logger.warn(marker, message, e)
     }
 
     override fun error0(message: String?, e: Throwable?) {
-        logger.error(message, e)
+        if (marker == null) logger.error(message, e)
+        else logger.error(marker, message, e)
     }
 
+    override val isVerboseEnabled: Boolean get() = logger.isTraceEnabled
+    override val isDebugEnabled: Boolean get() = logger.isDebugEnabled
+    override val isInfoEnabled: Boolean get() = logger.isInfoEnabled
+    override val isWarningEnabled: Boolean get() = logger.isWarnEnabled
+    override val isErrorEnabled: Boolean get() = logger.isErrorEnabled
+
     override val identity: String?
         get() = logger.name
 }
 
-internal class JdkLogger(private val logger: java.util.logging.Logger) : MiraiLoggerPlatformBase() {
+internal class JdkLoggerAdapter(private val logger: JulLogger) : MiraiLoggerPlatformBase() {
     override fun verbose0(message: String?, e: Throwable?) {
-        logger.log(Level.FINER, message, e)
+        logger.log(JulLevel.FINEST, message, e)
     }
 
     override fun debug0(message: String?, e: Throwable?) {
-        logger.log(Level.FINEST, message, e)
+        logger.log(JulLevel.FINER, message, e)
 
     }
 
     override fun info0(message: String?, e: Throwable?) {
-        logger.log(Level.INFO, message, e)
+        logger.log(JulLevel.INFO, message, e)
     }
 
     override fun warning0(message: String?, e: Throwable?) {
-        logger.log(Level.WARNING, message, e)
+        logger.log(JulLevel.WARNING, message, e)
     }
 
     override fun error0(message: String?, e: Throwable?) {
-        logger.log(Level.SEVERE, message, e)
+        logger.log(JulLevel.SEVERE, message, e)
     }
 
+    override val isVerboseEnabled: Boolean get() = logger.isLoggable(JulLevel.FINE)
+    override val isDebugEnabled: Boolean get() = logger.isLoggable(JulLevel.FINEST)
+    override val isInfoEnabled: Boolean get() = logger.isLoggable(JulLevel.INFO)
+    override val isWarningEnabled: Boolean get() = logger.isLoggable(JulLevel.WARNING)
+    override val isErrorEnabled: Boolean get() = logger.isLoggable(JulLevel.SEVERE)
+
     override val identity: String?
         get() = logger.name
-}
+}

+ 70 - 0
mirai-core-api/src/commonMain/kotlin/internal/utils/MarkedMiraiLogger.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.utils
+
+import net.mamoe.mirai.utils.MiraiLogger
+import org.apache.logging.log4j.Marker
+import org.apache.logging.log4j.MarkerManager
+
+/**
+ * 内部添加 [Marker] 支持, 并兼容旧 [MiraiLogger] API.
+ */
+internal interface MarkedMiraiLogger : MiraiLogger {
+    val marker: Marker?
+
+    /**
+     * Create an implementation-specific [MarkedMiraiLogger].
+     *
+     * Do not call the extension `MiraiLogger.subLogger` inside the function body.
+     */
+    fun subLogger(name: String): MarkedMiraiLogger
+}
+
+internal fun Marker(name: String, parents: Marker?): Marker {
+    return MarkerManager.getMarker(name).apply { if (parents != null) addParents(parents) }
+}
+
+internal fun Marker(name: String, vararg parents: Marker?): Marker {
+    return MarkerManager.getMarker(name).apply {
+        parents.forEach { if (it != null) addParents(it) }
+    }
+}
+
+internal val MiraiLogger.markerOrNull get() = (this as? MarkedMiraiLogger)?.marker
+
+/**
+ * Create a marked logger whose marker is a child of this' marker.
+ *
+ * Calling [MarkedMiraiLogger.subLogger] if possible, and creating [MiraiLoggerMarkedWrapper] otherwise.
+ */
+internal fun MiraiLogger.subLogger(name: String): MiraiLogger {
+    return subLoggerImpl(this, name)
+}
+
+// used by mirai-core
+internal fun subLoggerImpl(origin: MiraiLogger, name: String): MiraiLogger {
+    return if (origin is MarkedMiraiLogger) {
+        // origin can be Log4JAdapter or MiraiLoggerMarkedWrapper which delegates a non-Log4JAdapter.
+        origin.subLogger(name) // Log4JAdapter natively supports Markers.
+    } else {
+        return origin
+        // origin will never use the MiraiLoggerMarkedWrapper.marker so wrapping it is meaningless.
+    }
+}
+
+/**
+ * 仅当日志系统使用的不是 Log4J 时才会构造 [MiraiLoggerMarkedWrapper].
+ */
+private class MiraiLoggerMarkedWrapper(
+    val origin: MiraiLogger,
+    override val marker: Marker
+) : MiraiLogger by origin, MarkedMiraiLogger {
+    override fun subLogger(name: String): MarkedMiraiLogger = MiraiLoggerMarkedWrapper(origin, Marker(name, marker))
+}

+ 274 - 0
mirai-core-api/src/commonMain/kotlin/message/data/Audio.kt

@@ -0,0 +1,274 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused")
+@file:JvmMultifileClass
+@file:JvmName("MessageUtils")
+
+package net.mamoe.mirai.message.data
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.serializer
+import net.mamoe.mirai.contact.AudioSupported
+import net.mamoe.mirai.contact.Contact
+import net.mamoe.mirai.message.MessageSerializers
+import net.mamoe.mirai.message.data.MessageChain.Companion.serializeToJsonString
+import net.mamoe.mirai.utils.*
+
+/**
+ * 语音消息.
+ *
+ * [Audio] 分为 [OnlineAudio] 与 [OfflineAudio]. 在本地上传的, 或手动构造的语音为 [OfflineAudio]. 从服务器接收的语音为 [OnlineAudio].
+ *
+ * ## 上传和发送语音
+ *
+ * 使用 [AudioSupported.uploadAudio] 上传语音到服务器并取得 [Audio] 消息实例, 然后通过 [Contact.sendMessage] 发送.
+ *
+ * Java 示例:
+ * ```
+ * Audio audio;
+ * try {
+ *     audio = group.uploadAudio(resource); // 上传文件得到语音实例
+ * } finally {
+ *     resource.close(); // 保证资源正常关闭
+ * }
+ * group.sendMessage(audio); // 发送语音消息
+ * ```
+ *
+ * ## 下载语音
+ *
+ * 使用 [OnlineAudio.urlForDownload] 获取文件下载链接.
+ *
+ * ## [Audio] 与 [Voice] 的转换
+ *
+ * 原 [Voice] 已弃用故不推荐进行兼容转换. [Audio] 将有稳定性保证, 请尽量使用新的 [Audio].
+ *
+ * 将 [Audio] 转为 [Voice]: [Voice.fromAudio]
+ * 将 [Voice] 转为 [Audio]: [Voice.toAudio]
+ *
+ * @since 2.7
+ */
+public sealed interface Audio : MessageContent {
+    public companion object Key :
+        AbstractPolymorphicMessageKey<MessageContent, Audio>(MessageContent, { it.safeCast() })
+
+    /**
+     * 文件名称. 通常为 `XXX.amr`. 服务器要求文件名后缀必须为 ".amr", 但其[编码方式][codec]也有可能是非 [AudioCodec.AMR].
+     */
+    public val filename: String
+
+    /**
+     * 文件 MD5. 16 bytes.
+     */
+    public val fileMd5: ByteArray
+
+    /**
+     * 文件大小 bytes. 官方客户端支持最大文件大小约为 1MB, 过大的文件**可能**可以正常上传, 但在官方客户端无法收听 (显示文件损坏).
+     */
+    public val fileSize: Long
+
+    /**
+     * 编码方式.
+     *
+     * - 若语音文件真实编码方式为 [AudioCodec.SILK], 而该属性为 [AudioCodec.AMR], 语音文件将会被服务器压缩为低音质 [AudioCodec.AMR] 格式.
+     * - 若语音文件真实编码方式为 [AudioCodec.AMR], 而该属性为 [AudioCodec.SILK], 语音也可以正常发送并在客户端收听, 音质随文件真实格式而决定.
+     *
+     * 因此在发送时 [codec] 通常可以总是使用 [AudioCodec.SILK] (这也是 [AudioSupported.uploadAudio] 的默认行为).
+     */
+    public val codec: AudioCodec
+
+    /**
+     * 文件的额外数据. 该数据由服务器提供, 可能会影响语音音质等属性.
+     * [extraData] 为 `null` 时也可以发送语音, 但不确定发送和客户端收听是否会正常.
+     *
+     * [extraData] 可能随服务器更新而更新, 因此请不要尝试解析该数据.
+     *
+     * [extraData] 向下兼容, 即旧版本的 [extraData] 可以在新版本构造 [OfflineAudio] ([OfflineAudio.Factory.create]).
+     */
+    public val extraData: ByteArray?
+
+    /**
+     * @return `"[mirai:audio:${filename}]"`
+     */
+    public override fun toString(): String
+    public override fun contentToString(): String = "[语音消息]"
+}
+
+
+/**
+ * 在线语音消息, 即从消息事件中接收到的语音消息.
+ *
+ * [OnlineAudio] 可以获取[语音长度][length]以及[下载链接][urlForDownload].
+ *
+ * [OnlineAudio] 仅可以从事件中的[消息链][MessageChain]接收, 不可手动构造. 若需要手动构造, 请使用 [OfflineAudio.Factory.create] 构造 [离线语音][OfflineAudio].
+ *
+ * ### 序列化支持
+ *
+ * [OnlineAudio] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString].
+ * 也可以在 [MessageSerializers.serializersModule] 获取到 [OnlineAudio] 的 [KSerializer].
+ *
+ * 要获取更多有关序列化的信息, 参阅 [MessageSerializers].
+ *
+ * ### 不建议自行实现该接口
+ *
+ * [OnlineAudio] 不稳定, 将来可能会增加新的抽象属性或方法而导致不兼容. 仅可以使用该接口而不能继承或实现它.
+ *
+ * @since 2.7
+ * @see OfflineAudio
+ */
+@NotStableForInheritance
+public interface OnlineAudio : Audio { // 协议实现
+    /**
+     * 下载链接 HTTP URL.
+     * @return `"http://xxx"`
+     */
+    public val urlForDownload: String
+
+    /**
+     * 语音长度秒数
+     */
+    public val length: Long
+
+    public companion object Key :
+        AbstractPolymorphicMessageKey<Audio, OnlineAudio>(Audio, { it.safeCast() }) {
+
+        public const val SERIAL_NAME: String = "OnlineAudio"
+    }
+}
+
+/**
+ * 离线语音消息.
+ *
+ * [OfflineAudio] 仅拥有协议上必要的五个属性:
+ * - 文件名 [filename]
+ * - 文件 MD5 [fileMd5]
+ * - 文件大小 [fileSize]
+ * - 编码方式 [codec]
+ * - 额外数据 [extraData]
+ *
+ * [OfflineAudio] 可由本地 [ExternalResource] 经过 [AudioSupported.uploadAudio] 上传到服务器得到, 故无[下载链接][OnlineAudio.urlForDownload].
+ *
+ * [OfflineAudio] 同时还可以用做自定义构造 [Audio] 实例, 使用 [OfflineAudio.Factory.create] 可通过上述五个必要参数获得 [OfflineAudio] 实例.
+ *
+ * ### 序列化支持
+ *
+ * [OfflineAudio] 支持序列化. 可使用 [MessageChain.serializeToJsonString] 以及 [MessageChain.deserializeFromJsonString].
+ * 也可以在 [MessageSerializers.serializersModule] 获取到 [OfflineAudio] 的 [KSerializer].
+ *
+ * 要获取更多有关序列化的信息, 参阅 [MessageSerializers].
+ *
+ * ### 不建议自行实现该接口
+ *
+ * [OfflineAudio] 不稳定, 将来可能会增加新的抽象属性或方法而导致不兼容. 仅可以使用该接口而不能继承或实现它.
+ *
+ * @since 2.7
+ */
+@NotStableForInheritance
+public interface OfflineAudio : Audio {
+
+    public companion object Key :
+        AbstractPolymorphicMessageKey<Audio, OfflineAudio>(Audio, { it.safeCast() }) {
+        public const val SERIAL_NAME: String = "OfflineAudio"
+    }
+
+    public interface Factory {
+        /**
+         * 构造 [OfflineAudio]. 有关参数的含义, 参考 [Audio].
+         *
+         * 在 Kotlin 可以使用类构造器的函数 [OfflineAudio]: `OfflineAudio(...)`
+         */
+        public fun create(
+            filename: String,
+            fileMd5: ByteArray,
+            fileSize: Long,
+            codec: AudioCodec,
+            extraData: ByteArray?,
+        ): OfflineAudio
+
+        /**
+         * 使用 [OnlineAudio] 的信息构造 [OfflineAudio].
+         *
+         * 在 Kotlin 可以使用类构造器的函数 [OfflineAudio]: `OfflineAudio(...)`
+         */
+        public fun from(onlineAudio: OnlineAudio): OfflineAudio = onlineAudio.run {
+            create(filename, fileMd5, fileSize, codec, extraData)
+        }
+
+        public companion object INSTANCE :
+            Factory by loadService("net.mamoe.mirai.internal.message.OfflineAudioFactoryImpl")
+    }
+}
+
+/**
+ * 构造 [OfflineAudio]. 有关参数的含义, 参考 [Audio].
+ * @since 2.7
+ */
+@JvmSynthetic
+public inline fun OfflineAudio(
+    filename: String,
+    fileMd5: ByteArray,
+    fileSize: Long,
+    codec: AudioCodec,
+    extraData: ByteArray?,
+): OfflineAudio = OfflineAudio.Factory.create(filename, fileMd5, fileSize, codec, extraData)
+
+/**
+ * 使用 [OnlineAudio] 的信息构造 [OfflineAudio].
+ * @since 2.7
+ */
+@JvmSynthetic
+public inline fun OfflineAudio(
+    onlineAudio: OnlineAudio
+): OfflineAudio = OfflineAudio.Factory.from(onlineAudio)
+
+
+/**
+ * 语音编码方式.
+ *
+ * @since 2.7
+ */
+@Serializable(AudioCodec.AsIntSerializer::class)
+public enum class AudioCodec(
+    public val id: Int,
+    public val formatName: String,
+) {
+    /**
+     * 低音质编码格式
+     */
+    AMR(0, "amr"),
+
+    /**
+     * 高音质编码格式
+     */
+    SILK(1, "silk");
+
+    public companion object {
+        private val VALUES = values()
+
+        @JvmStatic
+        public fun fromId(id: Int): AudioCodec = VALUES.first { it.id == id }
+
+        @JvmStatic
+        public fun fromFormatName(formatName: String): AudioCodec = VALUES.first { it.formatName == formatName }
+
+        @JvmStatic
+        public fun fromIdOrNull(id: Int): AudioCodec? = VALUES.find { it.id == id }
+
+        @JvmStatic
+        public fun fromFormatNameOrNull(formatName: String): AudioCodec? =
+            VALUES.find { it.formatName == formatName }
+    }
+
+    internal object AsIntSerializer : KSerializer<AudioCodec> by Int.serializer().map(
+        Int.serializer().descriptor,
+        deserialize = { fromId(it) },
+        serialize = { id }
+    )
+}

+ 79 - 78
mirai-core-api/src/commonMain/kotlin/message/data/Voice.kt

@@ -7,16 +7,20 @@
  * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
+@file:JvmMultifileClass
+@file:JvmName("MessageUtils")
+@file:Suppress("NOTHING_TO_INLINE")
+
 package net.mamoe.mirai.message.data
 
-import kotlinx.serialization.KSerializer
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Transient
-import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.contact.Group
-import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.utils.ExternalResource.Companion.uploadAsVoice
+import net.mamoe.mirai.utils.MiraiExperimentalApi
+import net.mamoe.mirai.utils.MiraiInternalApi
+import net.mamoe.mirai.utils.NotStableForInheritance
+import net.mamoe.mirai.utils.safeCast
 
 
 /**
@@ -49,56 +53,68 @@ public abstract class PttMessage : MessageContent {
     @MiraiInternalApi
     @Transient
     public var pttInternalInstance: Any? = null
-        set(value) {
-            field = value
-            _pttInternalInstanceSerializeCache = null
-        }
-
-    @MiraiInternalApi
-    protected val pttInternalInstanceSerializeCache: String
-        get() {
-            _pttInternalInstanceSerializeCache?.let { return it }
-            return Mirai.serializePttElem(pttInternalInstance).also {
-                _pttInternalInstanceSerializeCache = it
-            }
-        }
-
-    @Transient
-    private var _pttInternalInstanceSerializeCache: String? = null
 }
 
 /**
- * 语音消息, 目前只支持接收和转发
+ * 已弃用的旧版本语音消息.
  *
- * 目前, 使用 [Voice] 类型是稳定的, 但调用 [Voice] 中的属性 [fileName], [md5], [fileSize] 是不稳定的. 语音的序列化也可能会在未来有变动.
+ * [Voice] 由于有设计缺陷已弃用且可能会在将来版本删除, 请使用 [Audio].
  *
- * ## 使用语音
+ * ## 迁移指南
  *
- * 可以通过 [ExternalResource.uploadAsVoice] 或者 [Group.uploadVoice] 上传语音文件到服务器, 得到 [Voice] 实例. 但这不会发送给目标群.
- * 上传后需要通过 [Group.sendMessage] 发送 [Voice] 实例.
- *
- * [Voice] 实例可以通过序列化方式保存. 下次可以用它发送因而不需要上传. 但可能由于未来服务器更新, 这项功能就不稳定. 因此建议总是上传音频文件而不要保存 [Voice].
+ * - 将使用的 [Voice] 类型替换为 [Audio] 类型
+ * - 将 [Group.uploadVoice] 替换为 [Group.uploadAudio]
+ * - 如果有必须使用旧 [Voice] 类型的情况, 请使用 [Audio.toVoice]
  */
-@Suppress("DuplicatedCode")
-@Serializable(Voice.Serializer::class) // experimental
+@Suppress("DuplicatedCode", "DEPRECATION")
+@Serializable
 @SerialName(Voice.SERIAL_NAME)
-public class Voice @MiraiInternalApi constructor(
+@Deprecated(
+    "Please use Audio instead.",
+    replaceWith = ReplaceWith("Audio", "net.mamoe.mirai.message.data.Audio"),
+    level = DeprecationLevel.WARNING
+)
+public open class Voice @MiraiInternalApi constructor(
     @MiraiExperimentalApi public override val fileName: String,
     @MiraiExperimentalApi public override val md5: ByteArray,
     @MiraiExperimentalApi public override val fileSize: Long,
 
-    @MiraiInternalApi public val codec: Int = 0,
+    @SerialName("codec") @MiraiInternalApi public val _codec: Int = 0,
     private val _url: String
 ) : PttMessage() {
 
     public companion object Key : AbstractPolymorphicMessageKey<PttMessage, Voice>(PttMessage, { it.safeCast() }) {
         public const val SERIAL_NAME: String = "Voice"
+
+        /**
+         * 将 2.7 新增的 [Audio] 转为旧版本的 [Voice], 以兼容某些情况.
+         *
+         * @see Audio.toVoice
+         * @since 2.7
+         */
+        @Suppress("DeprecatedCallableAddReplaceWith")
+        @Deprecated(
+            "Please consider migrating to Audio",
+            level = DeprecationLevel.WARNING
+        )
+        @JvmStatic
+        public fun fromAudio(audio: Audio): Voice {
+            audio.run {
+                return Voice(
+                    filename,
+                    fileMd5,
+                    fileSize,
+                    codec.id,
+                    if (this is OnlineAudio) kotlin.runCatching { urlForDownload }.getOrElse { "" } else ""
+                )
+            }
+        }
     }
 
     /**
      * 下载链接 HTTP URL.
      */
-    public val url: String?
+    public open val url: String?
         get() = when {
             _url.isBlank() -> null
             _url.startsWith("http") -> _url
@@ -115,69 +131,54 @@ public class Voice @MiraiInternalApi constructor(
 
     public override fun contentToString(): String = "[语音消息]"
 
+    /**
+     * 转换为 2.7 新增的 [Audio], 以兼容某些无法迁移的情况.
+     *
+     * @since 2.7
+     */
+    public fun toAudio(): Audio {
+        val voice = this
+        return OfflineAudio(
+            voice.fileName,
+            voice.md5,
+            voice.fileSize,
+            AudioCodec.fromIdOrNull(voice._codec) ?: AudioCodec.SILK,
+            byteArrayOf()
+        )
+    }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is Voice) return false
 
-        if (this.pttInternalInstance != null && other.pttInternalInstance != null) {
-            if (this.pttInternalInstance == other.pttInternalInstance)
-                return true
-            // strict
-            return this.pttInternalInstanceSerializeCache == other.pttInternalInstanceSerializeCache
-        }
-
         if (fileName != other.fileName) return false
         if (!md5.contentEquals(other.md5)) return false
         if (fileSize != other.fileSize) return false
-        if (codec != other.codec) return false
+        if (_codec != other._codec) return false
         if (_url != other._url) return false
 
         return true
     }
 
     override fun hashCode(): Int {
-        if (pttInternalInstance != null)
-            return pttInternalInstanceSerializeCache.hashCode()
-
         var result = fileName.hashCode()
         result = 12 * result + md5.contentHashCode()
         result = 54 * result + fileSize.hashCode()
-        result = 33 * result + codec
+        result = 33 * result + _codec
         result = 15 * result + _url.hashCode()
         return result
     }
+}
 
-    public object Serializer : KSerializer<Voice> by VoiceS.serializer().map(
-        resultantDescriptor = VoiceS.serializer().descriptor.copy(SERIAL_NAME),
-        deserialize = {
-            Voice(
-                fileName = it.fileName,
-                md5 = it.md5,
-                fileSize = it.fileSize,
-                codec = it.codec,
-                _url = it._url,
-            ).also { v -> v.pttInternalInstance = Mirai.deserializePttElem(it.ptt) }
-        },
-        serialize = {
-            VoiceS(
-                fileName = it.fileName,
-                md5 = it.md5,
-                fileSize = it.fileSize,
-                _url = it._url,
-                codec = it.codec,
-                ptt = Mirai.serializePttElem(it.pttInternalInstance)
-            )
-        }
-    ) {
-        @Serializable
-        @SerialName(SERIAL_NAME)
-        private class VoiceS(
-            val fileName: String,
-            val md5: ByteArray,
-            val fileSize: Long,
-            val codec: Int,
-            val _url: String,
-            val ptt: String = "",
-        )
-    }
-}
+/**
+ * 将 2.7 新增的 [Audio] 转为旧版本的 [Voice], 以兼容某些情况.
+ *
+ * @since 2.7
+ */
+@Suppress("DEPRECATION", "DeprecatedCallableAddReplaceWith")
+@Deprecated(
+    "Please migrate to Audio",
+    level = DeprecationLevel.WARNING
+)
+@JvmSynthetic
+public inline fun Audio.toVoice(): Voice = Voice.fromAudio(this)

+ 6 - 2
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -326,7 +326,9 @@ public open class BotConfiguration { // open for Java
      *
      * @see MiraiLogger
      */
-    public var botLoggerSupplier: ((Bot) -> MiraiLogger) = { MiraiLogger.create("Bot ${it.id}") }
+    public var botLoggerSupplier: ((Bot) -> MiraiLogger) = {
+        MiraiLogger.Factory.create(Bot::class, "Bot ${it.id}")
+    }
 
     /**
      * 网络层日志构造器
@@ -338,7 +340,9 @@ public open class BotConfiguration { // open for Java
      *
      * @see MiraiLogger
      */
-    public var networkLoggerSupplier: ((Bot) -> MiraiLogger) = { MiraiLogger.create("Net ${it.id}") }
+    public var networkLoggerSupplier: ((Bot) -> MiraiLogger) = {
+        MiraiLogger.Factory.create(Bot::class, "Net ${it.id}")
+    }
 
 
     /**

+ 1 - 0
mirai-core-api/src/commonMain/kotlin/utils/DeviceInfo.kt

@@ -60,6 +60,7 @@ public class DeviceInfo(
     )
 
     public companion object {
+        internal val logger = MiraiLogger.Factory.create(DeviceInfo::class, "DeviceInfo")
 
         /**
          * 加载一个设备信息. 若文件不存在或为空则随机并创建一个设备信息保存.

+ 6 - 50
mirai-core-api/src/commonMain/kotlin/utils/ExternalResource.kt

@@ -19,13 +19,12 @@ 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.VoiceSupported
+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
@@ -392,58 +391,15 @@ public interface ExternalResource : Closeable {
         // region uploadAsVoice
         ///////////////////////////////////////////////////////////////////////////
 
-        /**
-         * 将文件作为语音上传后构造 [Voice]. 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友.
-         *
-         * **服务器仅支持音频格式 `silk` 或 `amr`**. 需要调用方手动[关闭资源][ExternalResource.close].
-         *
-         * @throws OverFileSizeMaxException
-         * @since 2.7
-         * @see VoiceSupported.uploadVoice
-         */
+        @Suppress("DEPRECATION")
         @JvmBlockingBridge
         @JvmStatic
-        public suspend fun ExternalResource.uploadAsVoice(contact: VoiceSupported): Voice {
-            return contact.uploadVoice(this)
-        }
-
-        @JvmBlockingBridge
-        @JvmStatic
-        @Deprecated("For binary compatibility", level = DeprecationLevel.WARNING)
-        @JvmName("uploadAsVoice")
-        public suspend fun ExternalResource.uploadAsVoice(contact: Contact): Voice {
-            if (contact is VoiceSupported) return contact.uploadVoice(this)
+        @Deprecated("Use `contact.uploadAudio(resource)` instead", level = DeprecationLevel.WARNING)
+        public suspend fun ExternalResource.uploadAsVoice(contact: Contact): net.mamoe.mirai.message.data.Voice {
+            @Suppress("DEPRECATION")
+            if (contact is Group) return contact.uploadVoice(this)
             else throw UnsupportedOperationException("Contact `$contact` is not supported uploading voice")
         }
-
-        /**
-         * 读取 [InputStream] 到临时文件并将其作为语音上传至 [contact] 后构造 [Voice],
-         * 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友
-         *
-         * 注意:本函数不会关闭流.
-         *
-         * @since 2.7
-         * @throws OverFileSizeMaxException
-         * @see VoiceSupported.uploadVoice
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        public suspend fun InputStream.uploadAsVoice(contact: VoiceSupported): Voice =
-            runBIO { toExternalResource() }.withUse { uploadAsVoice(contact) }
-
-        /**
-         * 将文件作为语音上传后构造 [Voice].
-         * 上传后只会得到 [Voice] 实例, 而不会将语音发送到目标群或好友
-         *
-         * @since 2.7
-         * @throws OverFileSizeMaxException
-         * @see VoiceSupported.uploadVoice
-         */
-        @JvmStatic
-        @JvmBlockingBridge
-        public suspend fun File.uploadAsVoice(contact: VoiceSupported): Voice =
-            toExternalResource().withUse { uploadAsVoice(contact) }
-
         // endregion
     }
 }

+ 52 - 6
mirai-core-api/src/commonMain/kotlin/utils/LoggerAdapters.kt

@@ -7,25 +7,71 @@
  *  https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("unused")
+
 package net.mamoe.mirai.utils
 
-import net.mamoe.mirai.internal.utils.JdkLogger
-import net.mamoe.mirai.internal.utils.Log4jLogger
-import net.mamoe.mirai.internal.utils.Slf4jLogger
+import net.mamoe.mirai.internal.utils.JdkLoggerAdapter
+import net.mamoe.mirai.internal.utils.Log4jLoggerAdapter
+import net.mamoe.mirai.internal.utils.MARKER_MIRAI
+import net.mamoe.mirai.internal.utils.Slf4jLoggerAdapter
+import org.apache.logging.log4j.LogManager
+import org.apache.logging.log4j.Marker
+import org.apache.logging.log4j.MarkerManager
 
+/**
+ * [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 到 [MiraiLogger] 的转换器.
+ */
 public object LoggerAdapters {
+    /**
+     * 使用 [LOG4J2][org.apache.logging.log4j.Logger] 接管全局 Mirai 日志系统. 请在调用 Mirai API 任何其他 API 前调用该方法.
+     *
+     * 注意, 若已经通过 service 方式提供 [MiraiLogger.Factory] 来接管日志系统, 则本方法无效.
+     *
+     * @since 2.7
+     */
+    @JvmStatic
+    @MiraiExperimentalApi
+    public fun useLog4j2() {
+        DefaultFactoryOverrides.override { requester, identity ->
+            val logger = LogManager.getLogger(requester)
+            Log4jLoggerAdapter(logger, MarkerManager.getMarker(identity ?: logger.name).addParents(MARKER_MIRAI))
+        }
+    }
+
+
+    /**
+     * 将 [java.util.logging.Logger] 转换作为 [MiraiLogger] 使用.
+     */
     @JvmStatic
     public fun java.util.logging.Logger.asMiraiLogger(): MiraiLogger {
-        return JdkLogger(this)
+        return JdkLoggerAdapter(this)
     }
 
+    /**
+     * 将 [org.apache.logging.log4j.Logger] 转换作为 [MiraiLogger] 使用.
+     */
     @JvmStatic
     public fun org.apache.logging.log4j.Logger.asMiraiLogger(): MiraiLogger {
-        return Log4jLogger(this)
+        return Log4jLoggerAdapter(this, null)
     }
 
+    /**
+     * 将 [org.slf4j.Logger] 转换作为 [MiraiLogger] 使用.
+     */
     @JvmStatic
     public fun org.slf4j.Logger.asMiraiLogger(): MiraiLogger {
-        return Slf4jLogger(this)
+        return Slf4jLoggerAdapter(this, null)
+    }
+
+    /**
+     * 将 [org.apache.logging.log4j.Logger] 转换作为 [MiraiLogger] 使用.
+     *
+     * @since 2.7
+     */
+    @MiraiExperimentalApi
+    @JvmStatic
+    public fun org.apache.logging.log4j.Logger.asMiraiLogger(marker: Marker): MiraiLogger {
+        return Log4jLoggerAdapter(this, marker)
     }
 }

+ 174 - 33
mirai-core-api/src/commonMain/kotlin/utils/MiraiLogger.kt

@@ -13,7 +13,8 @@
 
 package net.mamoe.mirai.utils
 
-import net.mamoe.mirai.Bot
+import java.util.*
+import kotlin.reflect.KClass
 
 /**
  * 给这个 logger 添加一个开关, 用于控制是否记录 log
@@ -22,57 +23,106 @@ import net.mamoe.mirai.Bot
 public fun MiraiLogger.withSwitch(default: Boolean = true): MiraiLoggerWithSwitch = MiraiLoggerWithSwitch(this, default)
 
 /**
- * 日志记录器. 所有的输出均依赖于它.
- * 不同的对象可拥有只属于自己的 logger. 通过 [identity] 来区分.
+ * 日志记录器.
  *
- * 注意: 如果你需要重新实现日志, 请不要直接实现这个接口, 请继承 [MiraiLoggerPlatformBase]
+ * ## Mirai 日志系统
  *
- * 在定义 logger 变量时, 请一直使用 [MiraiLogger] 或者 [MiraiLoggerWithSwitch].
+ * Mirai 内建简单的日志系统, 即 [MiraiLogger]. [MiraiLogger] 的实现有 [SimpleLogger], [PlatformLogger], [SilentLogger].
+ *
+ * [MiraiLogger] 仅能处理简单的日志任务, 通常推荐使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 等日志库.
+ *
+ * ## 使用第三方日志库接管 Mirai 日志系统
+ *
+ * 使用 [LoggerAdapters], 将第三方日志 `Logger` 转为 [MiraiLogger]. 然后通过 [MiraiLogger.setDefaultLoggerCreator] 全局覆盖日志.
+ *
+ * ## 实现或使用 [MiraiLogger]
  *
- * Mirai 内建三种日志实现, 分别是 [SimpleLogger], [PlatformLogger], [SilentLogger]
+ * 不建议实现或使用 [MiraiLogger]. 请优先考虑使用上述第三方框架. [MiraiLogger] 仅应用于兼容旧版本代码.
  *
  * @see SimpleLogger 简易 logger, 它将所有的日志记录操作都转移给 lambda `(String?, Throwable?) -> Unit`
  * @see PlatformLogger 各个平台下的默认日志记录实现.
  * @see SilentLogger 忽略任何日志记录操作的 logger 实例.
+ * @see LoggerAdapters
  *
  * @see MiraiLoggerPlatformBase 平台通用基础实现. 若 Mirai 自带的日志系统无法满足需求, 请继承这个类并实现其抽象函数.
  */
 public interface MiraiLogger {
 
+    /**
+     * 可以 service 实现的方式覆盖.
+     *
+     * @since 2.7
+     */
+    public interface Factory {
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象的 class
+         * @param identity 对象标记 (备注)
+         */
+        public fun create(requester: KClass<*>, identity: String? = null): MiraiLogger =
+            this.create(requester.java, identity)
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象的 class
+         * @param identity 对象标记 (备注)
+         */
+        public fun create(requester: Class<*>, identity: String? = null): MiraiLogger
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象
+         */
+        public fun create(requester: KClass<*>): MiraiLogger = create(requester, null)
+
+        /**
+         * 创建 [MiraiLogger] 实例.
+         *
+         * @param requester 请求创建 [MiraiLogger] 的对象
+         */
+        public fun create(requester: Class<*>): MiraiLogger = create(requester, null)
+
+        public companion object INSTANCE : Factory by loadService(Factory::class, { DefaultFactory() })
+    }
+
     public companion object {
         /**
          * 顶层日志, 仅供 Mirai 内部使用.
          */
         @MiraiInternalApi
         @MiraiExperimentalApi
-        public val TopLevel: MiraiLogger by lazy { create("Mirai") }
-
-        @Volatile
-        private var defaultLogger: (identity: String?) -> MiraiLogger = { PlatformLogger(it) }
+        @Deprecated("Deprecated.")
+        public val TopLevel: MiraiLogger by lazy { Factory.create(MiraiLogger::class, "Mirai") }
 
         /**
-         * 可直接修改这个变量的值来重定向日志输出.
+         * 已弃用, 请实现 service [net.mamoe.mirai.utils.MiraiLogger.Factory] 并以 [ServiceLoader] 支持的方式提供.
          */
+        @Suppress("DeprecatedCallableAddReplaceWith")
+        @Deprecated(
+            "Please set factory by providing an service of type net.mamoe.mirai.utils.MiraiLogger.Factory",
+            level = DeprecationLevel.WARNING
+        )
         @JvmStatic
         public fun setDefaultLoggerCreator(creator: (identity: String?) -> MiraiLogger) {
-            defaultLogger = creator
+            DefaultFactoryOverrides.override { _, identity -> creator(identity) }
         }
 
         /**
-         * 用于创建默认的日志记录器. 在一些需要使用日志的 Mirai 的组件, 如 [Bot], 都会通过这个函数构造日志记录器.
-         *
-         * **注意:** 请务必将所有的输出定向到日志记录系统, 否则在某些情况下 (如 web 控制台中) 将无法接收到输出
-         *
-         * **注意:** 请为日志做好分类, 即不同的模块使用不同的 [MiraiLogger].
-         * 如, [Bot] 中使用 `identity` 为 "Bot(qqId)" 的 [MiraiLogger]
-         * 而 [Bot] 的网络处理中使用 `identity` 为 "BotNetworkHandler".
+         * 旧版本用于创建 [MiraiLogger]. 已弃用. 请使用 [MiraiLogger.Factory.INSTANCE.create].
          *
          * @see setDefaultLoggerCreator
          */
+        @Deprecated(
+            "Please use MiraiLogger.Factory.create", ReplaceWith(
+                "MiraiLogger.Factory.create(YourClass::class, identity)",
+                "net.mamoe.mirai.utils.MiraiLogger"
+            ), level = DeprecationLevel.WARNING
+        )
         @JvmStatic
-        public fun create(identity: String?): MiraiLogger {
-            return defaultLogger.invoke(identity)
-        }
+        public fun create(identity: String?): MiraiLogger = Factory.create(MiraiLogger::class, identity)
     }
 
     /**
@@ -92,6 +142,61 @@ public interface MiraiLogger {
      */
     public val isEnabled: Boolean
 
+    /**
+     * 当 VERBOSE 级别的日志启用时返回 `true`.
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public val isVerboseEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 DEBUG 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public val isDebugEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 INFO 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public val isInfoEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 WARNING 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public val isWarningEnabled: Boolean get() = isEnabled
+
+    /**
+     * 当 ERROR 级别的日志启用时返回 `true`
+     *
+     * 若 [isEnabled] 为 `false`, 返回 `false`.
+     * 在使用 [SLF4J][org.slf4j.Logger], [LOG4J][org.apache.logging.log4j.Logger] 或 [JUL][java.util.logging.Logger] 时返回真实配置值.
+     * 其他情况下返回 [isEnabled] 的值.
+     *
+     * @since 2.7
+     */
+    public val isErrorEnabled: Boolean get() = isEnabled
+
     /**
      * 随从. 在 this 中调用所有方法后都应继续往 [follower] 传递调用.
      * [follower] 的存在可以让一次日志被多个日志记录器记录.
@@ -102,7 +207,11 @@ public interface MiraiLogger {
      *
      * 当然, 多个 logger 也可以加在一起: `val logger = bot.logger + MynLogger() + MyLogger2()`
      */
+    @Suppress("UNUSED_PARAMETER")
+    @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.WARNING) // deprecated since 2.7
     public var follower: MiraiLogger?
+        get() = null
+        set(value) {}
 
     /**
      * 记录一个 `verbose` 级别的日志.
@@ -164,48 +273,50 @@ public interface MiraiLogger {
      *
      * @return [follower]
      */
-    public operator fun <T : MiraiLogger> plus(follower: T): T
+    @Suppress("DeprecatedCallableAddReplaceWith")
+    @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.WARNING) // deprecated since 2.7
+    public operator fun <T : MiraiLogger> plus(follower: T): T = follower
 }
 
 
 public inline fun MiraiLogger.verbose(message: () -> String) {
-    if (isEnabled) verbose(message())
+    if (isVerboseEnabled) verbose(message())
 }
 
 public inline fun MiraiLogger.verbose(message: () -> String, e: Throwable?) {
-    if (isEnabled) verbose(message(), e)
+    if (isVerboseEnabled) verbose(message(), e)
 }
 
 public inline fun MiraiLogger.debug(message: () -> String?) {
-    if (isEnabled) debug(message())
+    if (isDebugEnabled) debug(message())
 }
 
 public inline fun MiraiLogger.debug(message: () -> String?, e: Throwable?) {
-    if (isEnabled) debug(message(), e)
+    if (isDebugEnabled) debug(message(), e)
 }
 
 public inline fun MiraiLogger.info(message: () -> String?) {
-    if (isEnabled) info(message())
+    if (isInfoEnabled) info(message())
 }
 
 public inline fun MiraiLogger.info(message: () -> String?, e: Throwable?) {
-    if (isEnabled) info(message(), e)
+    if (isInfoEnabled) info(message(), e)
 }
 
 public inline fun MiraiLogger.warning(message: () -> String?) {
-    if (isEnabled) warning(message())
+    if (isWarningEnabled) warning(message())
 }
 
 public inline fun MiraiLogger.warning(message: () -> String?, e: Throwable?) {
-    if (isEnabled) warning(message(), e)
+    if (isWarningEnabled) warning(message(), e)
 }
 
 public inline fun MiraiLogger.error(message: () -> String?) {
-    if (isEnabled) error(message())
+    if (isErrorEnabled) error(message())
 }
 
 public inline fun MiraiLogger.error(message: () -> String?, e: Throwable?) {
-    if (isEnabled) error(message(), e)
+    if (isErrorEnabled) error(message(), e)
 }
 
 /**
@@ -353,8 +464,12 @@ public class MiraiLoggerWithSwitch internal constructor(private val delegate: Mi
  * @see PlatformLogger
  * @see SimpleLogger
  */
+@Suppress("DEPRECATION")
 public abstract class MiraiLoggerPlatformBase : MiraiLogger {
     public override val isEnabled: Boolean get() = true
+
+    @Suppress("OverridingDeprecatedMember")
+    @Deprecated("follower 设计不佳, 请避免使用", level = DeprecationLevel.WARNING) // deprecated since 2.7
     public final override var follower: MiraiLogger? = null
 
     public final override fun verbose(message: String?) {
@@ -428,8 +543,34 @@ public abstract class MiraiLoggerPlatformBase : MiraiLogger {
     protected open fun error0(message: String?): Unit = error0(message, null)
     protected abstract fun error0(message: String?, e: Throwable?)
 
+    @Suppress("OverridingDeprecatedMember")
+    @Deprecated("plus 设计不佳, 请避免使用.", level = DeprecationLevel.WARNING) // deprecated since 2.7
     public override operator fun <T : MiraiLogger> plus(follower: T): T {
         this.follower = follower
         return follower
     }
 }
+
+internal object DefaultFactoryOverrides {
+    var override: ((requester: Class<*>, identity: String?) -> MiraiLogger)? =
+        null // 支持 LoggerAdapters 以及兼容旧版本
+
+    @JvmStatic
+    fun override(lambda: (requester: Class<*>, identity: String?) -> MiraiLogger) {
+        override = lambda
+    }
+
+    @JvmStatic
+    fun clearOverride() {
+        override = null
+    }
+}
+
+internal class DefaultFactory : MiraiLogger.Factory {
+    override fun create(requester: Class<*>, identity: String?): MiraiLogger {
+        val override = DefaultFactoryOverrides.override
+        return if (override != null) override(requester, identity) else PlatformLogger(
+            identity ?: requester.kotlin.simpleName ?: requester.simpleName
+        )
+    }
+}

+ 91 - 0
mirai-core-api/src/commonTest/kotlin/logging/Log4j2LoggingTest.kt

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.logging
+
+import net.mamoe.mirai.Bot
+import net.mamoe.mirai.internal.utils.*
+import net.mamoe.mirai.utils.DefaultFactoryOverrides
+import net.mamoe.mirai.utils.LoggerAdapters.asMiraiLogger
+import net.mamoe.mirai.utils.MiraiLogger
+import org.apache.logging.log4j.LogManager
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertSame
+
+internal class Log4j2LoggingTest {
+    @BeforeEach
+    fun init() {
+        DefaultFactoryOverrides.override { requester, identity ->
+            LogManager.getLogger(requester).asMiraiLogger(Marker(identity ?: requester.simpleName, MARKER_MIRAI))
+        }
+    }
+
+    @AfterEach
+    fun cleanup() {
+        DefaultFactoryOverrides.clearOverride()
+    }
+
+    private fun MiraiLogger.cast(): Log4jLoggerAdapter = this as Log4jLoggerAdapter
+
+    @Test
+    fun `created is Log4jLoggerAdapter`() {
+        val logger = MiraiLogger.Factory.create(Log4j2LoggingTest::class, "test1")
+        assertIs<Log4jLoggerAdapter>(logger)
+    }
+
+    @Test
+    fun `identity is considered as marker`() {
+        val logger = MiraiLogger.Factory.create(Log4j2LoggingTest::class, "test1")
+        assertEquals("test1", logger.cast().marker!!.name)
+    }
+
+
+    @Test
+    fun `test subLogger Marker`() {
+        val parent = MiraiLogger.Factory.create(Log4j2LoggingTest::class, "test1")
+        val parentMarker = parent.cast().marker!!
+
+        val child = parent.subLogger("sub")
+        val childMarker = child.markerOrNull!!
+
+        assertEquals("test1", parentMarker.name)
+        assertEquals("sub", childMarker.name)
+
+        assertSame(parentMarker, childMarker.parents.single())
+        assertSame("test1", childMarker.parents.single().name)
+    }
+
+    @Test
+    fun `test subLogger Marker 2`() {
+        val parent = MiraiLogger.Factory.create(Log4j2LoggingTest::class, "test1")
+        val parentMarker = parent.cast().marker!!
+
+        val child = parent.subLogger("sub").subLogger("sub2")
+        val childMarker = child.markerOrNull!!
+
+        assertEquals("test1", parentMarker.name)
+        assertEquals("sub2", childMarker.name)
+
+        assertSame("sub", childMarker.parents.single().name)
+        assertSame(parentMarker, childMarker.parents.single().parents.single())
+    }
+
+    @Test
+    fun `logging output test`() {
+        val logger = LogManager.getLogger(Bot::class.java)
+        logger.info("Test")
+        MiraiLogger.Factory.create(Bot::class).run {
+            info("InfoFF")
+        }
+    }
+}

+ 36 - 0
mirai-core-api/src/commonTest/kotlin/logging/LoggingCompatibilityTest.kt

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.logging
+
+import net.mamoe.mirai.utils.MiraiLogger
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+
+internal class LoggingCompatibilityTest {
+
+    @Suppress("DEPRECATION")
+    @Test
+    fun `legacy overrides are still working if no services are found`() {
+        val messages = StringBuilder()
+
+        MiraiLogger.setDefaultLoggerCreator {
+            net.mamoe.mirai.utils.SimpleLogger("my logger") { message: String?, _: Throwable? ->
+                messages.append(message)
+            }
+        }
+
+        val created = MiraiLogger.Factory.create(this::class)
+        assertIs<MiraiLogger>(created)
+        created.info("test")
+
+        assertEquals("test", messages.toString().trim())
+    }
+}

+ 13 - 0
mirai-core-api/src/commonTest/resources/log4j.properties

@@ -0,0 +1,13 @@
+#
+# Copyright 2019-2021 Mamoe Technologies and contributors.
+#
+# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+#
+# https://github.com/mamoe/mirai/blob/dev/LICENSE
+#
+log4j.rootLogger=INFO,stdout
+log4j.logger.mirai=INFO
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%p\t%d{ISO8601}\t%r\t%c\t%m%n

+ 5 - 3
mirai-core-api/src/jvmMain/kotlin/utils/SwingSolver.kt

@@ -14,7 +14,9 @@ package net.mamoe.mirai.utils
  * @author Karlatemp <[email protected]> <https://github.com/Karlatemp>
  */
 
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 import net.mamoe.mirai.Bot
 import java.awt.*
 import java.awt.event.*
@@ -109,8 +111,8 @@ public object SwingSolver : LoginSolver() {
                 """
                 <html>
                 需要进行账户安全认证<br>
-                该账户有[设备锁]/[不常用登录地点]/[不常用设备登录]的问题<br>
-                完成以下账号认证即可成功登录|理论本认证在mirai每个账户中最多出现1次<br>
+                该账户有设备锁/不常用登录地点/不常用设备登录的问题<br>
+                请在<b>手机 QQ</b> 打开下面链接
                 成功后请关闭该窗口
             """.trimIndent()
             )

+ 23 - 0
mirai-core-utils/src/androidMain/kotlin/Actuals.kt

@@ -13,6 +13,9 @@
 package net.mamoe.mirai.utils
 
 import android.util.Base64
+import java.util.*
+import kotlin.reflect.KClass
+import kotlin.reflect.full.createInstance
 
 
 public actual fun ByteArray.encodeBase64(): String {
@@ -40,3 +43,23 @@ public actual inline fun <reified E> Throwable.unwrap(): Throwable {
         ?.also { it.addSuppressed(e) }
         ?: this
 }
+
+public actual fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String?): T {
+    var suppressed: Throwable? = null
+    return ServiceLoader.load(clazz.java).firstOrNull()
+        ?: (if (fallbackImplementation == null) null
+        else runCatching { findCreateInstance<T>(fallbackImplementation) }.onFailure { suppressed = it }.getOrNull())
+        ?: throw NoSuchElementException("Could not find an implementation for service class ${clazz.qualifiedName}").apply {
+            if (suppressed != null) addSuppressed(suppressed)
+        }
+}
+
+private fun <T : Any> findCreateInstance(fallbackImplementation: String): T {
+    return Class.forName(fallbackImplementation).cast<Class<out T>>().kotlin.run { objectInstance ?: createInstance() }
+}
+
+public actual fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String?): T? {
+    return ServiceLoader.load(clazz.java).firstOrNull()
+        ?: if (fallbackImplementation == null) return null
+        else runCatching { findCreateInstance<T>(fallbackImplementation) }.getOrNull()
+}

+ 2 - 2
mirai-core-utils/src/commonMain/kotlin/Annotations.kt

@@ -14,6 +14,6 @@ import kotlin.annotation.AnnotationTarget.*
 
 
 @RequiresOptIn("This can only be used in tests.", level = ERROR)
-@Target(CLASS, FUNCTION, PROPERTY)
-@Retention(AnnotationRetention.BINARY)
+@Target(CLASS, FUNCTION, PROPERTY, CLASS, CONSTRUCTOR, FUNCTION)
+@Retention(AnnotationRetention.SOURCE)
 public annotation class TestOnly

+ 1 - 1
mirai-core-utils/src/commonMain/kotlin/Bytes.kt

@@ -149,7 +149,7 @@ 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)
+    String(this, charset = charset, offset = offset, length = this.size - offset)
 
 public expect fun ByteArray.encodeBase64(): String
 public expect fun String.decodeBase64(): ByteArray

+ 49 - 0
mirai-core-utils/src/commonMain/kotlin/ComputeOnNullMutableProperty.kt

@@ -0,0 +1,49 @@
+/*
+ * 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.utils
+
+import kotlinx.atomicfu.atomic
+import kotlin.reflect.KProperty
+
+public fun <T : Any> computeOnNullMutableProperty(initializer: () -> T): ComputeOnNullMutableProperty<T> =
+    ComputeOnNullMutablePropertyImpl(initializer)
+
+public interface ComputeOnNullMutableProperty<V : Any> {
+    public fun get(): V
+    public fun set(value: V?)
+
+    public operator fun getValue(thisRef: Any?, property: KProperty<*>): V = get()
+    public operator fun setValue(thisRef: Any?, property: KProperty<*>, value: V?): Unit = set(value)
+}
+
+
+private class ComputeOnNullMutablePropertyImpl<T : Any>(
+    private val initializer: () -> T
+) : ComputeOnNullMutableProperty<T> {
+    private val value = atomic<T?>(null)
+
+    override tailrec fun get(): T {
+        return when (val v = this.value.value) {
+            null -> synchronized(this) {
+                if (this.value.value === null) {
+                    val value = this.initializer()
+                    // compiler inserts
+                    this.value.compareAndSet(null, value) // setValue prevails
+                    return get()
+                } else this.value.value as T
+            }
+            else -> v
+        }
+    }
+
+    override fun set(value: T?) {
+        this.value.value = value
+    }
+}

+ 22 - 0
mirai-core-utils/src/commonMain/kotlin/Services.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import kotlin.reflect.KClass
+
+public expect fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String? = null): T?
+public expect fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String? = null): T
+
+public inline fun <reified T : Any> loadService(fallbackImplementation: String? = null): T =
+    loadService(T::class, fallbackImplementation)
+
+// do not inline: T will be inferred to returning type of `fallbackImplementation`
+public fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: () -> T): T =
+    loadServiceOrNull(clazz) ?: fallbackImplementation()

+ 51 - 0
mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/ComputeOnNullMutablePropertyTest.kt

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.utils
+
+import org.junit.jupiter.api.Test
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+internal class ComputeOnNullMutablePropertyTest {
+    @Test
+    fun `can initialize`() {
+        val prop = computeOnNullMutableProperty { "ok" }
+        assertEquals("ok", prop.get())
+    }
+
+    @Test
+    fun `can override`() {
+        val called = AtomicBoolean(false)
+        val prop = computeOnNullMutableProperty { "not ok".also { called.set(true) } }
+        prop.set("ok")
+        assertEquals("ok", prop.get())
+        assertFalse { called.get() }
+    }
+
+    @Test
+    fun `can reinitialize 1`() {
+        val called = AtomicBoolean(false)
+        val prop = computeOnNullMutableProperty { "ok".also { called.set(true) } }
+        prop.set("not ok 2")
+        prop.set(null)
+        assertEquals("ok", prop.get())
+        assertTrue { called.get() }
+    }
+
+    @Test
+    fun `can reinitialize 2`() {
+        val prop = computeOnNullMutableProperty { "ok" }
+        prop.get()
+        prop.set(null)
+        assertEquals("ok", prop.get())
+    }
+}

+ 22 - 0
mirai-core-utils/src/jvmMain/kotlin/Actuals.kt

@@ -13,6 +13,8 @@
 package net.mamoe.mirai.utils
 
 import java.util.*
+import kotlin.reflect.KClass
+import kotlin.reflect.full.createInstance
 
 
 public actual fun ByteArray.encodeBase64(): String {
@@ -29,3 +31,23 @@ public actual inline fun <reified E> Throwable.unwrap(): Throwable {
         ?.also { it.addSuppressed(this) }
         ?: this
 }
+
+public actual fun <T : Any> loadService(clazz: KClass<out T>, fallbackImplementation: String?): T {
+    var suppressed: Throwable? = null
+    return ServiceLoader.load(clazz.java).firstOrNull()
+        ?: (if (fallbackImplementation == null) null
+        else runCatching { findCreateInstance<T>(fallbackImplementation) }.onFailure { suppressed = it }.getOrNull())
+        ?: throw NoSuchElementException("Could not find an implementation for service class ${clazz.qualifiedName}").apply {
+            if (suppressed != null) addSuppressed(suppressed)
+        }
+}
+
+private fun <T : Any> findCreateInstance(fallbackImplementation: String): T {
+    return Class.forName(fallbackImplementation).cast<Class<out T>>().kotlin.run { objectInstance ?: createInstance() }
+}
+
+public actual fun <T : Any> loadServiceOrNull(clazz: KClass<out T>, fallbackImplementation: String?): T? {
+    return ServiceLoader.load(clazz.java).firstOrNull()
+        ?: if (fallbackImplementation == null) return null
+        else runCatching { findCreateInstance<T>(fallbackImplementation) }.getOrNull()
+}

+ 1 - 1
mirai-core/build.gradle.kts

@@ -72,13 +72,13 @@ kotlin {
                 api1(`kotlinx-io-jvm`)
                 implementation1(`kotlinx-coroutines-io`)
                 implementation(`netty-all`)
+                implementation(`log4j-api`)
             }
         }
 
         commonTest {
             dependencies {
                 implementation(kotlin("script-runtime"))
-                runtimeOnly(`slf4j-simple`)
             }
         }
 

+ 1 - 1
mirai-core/src/androidTest/kotlin/test/initPlatform.android.kt

@@ -34,7 +34,7 @@ internal actual class PlatformInitializationTest : AbstractTest() {
     actual fun test() {
         assertTrue {
             @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
-            MiraiLogger.create("1") is net.mamoe.mirai.internal.utils.StdoutLogger
+            MiraiLogger.Factory.create(this::class, "1") is net.mamoe.mirai.internal.utils.StdoutLogger
         }
     }
 }

+ 8 - 10
mirai-core/src/commonMain/kotlin/MiraiImpl.kt

@@ -124,6 +124,14 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
                 UnsupportedMessageImpl::class,
                 UnsupportedMessageImpl.serializer()
             )
+            MessageSerializers.registerSerializer(
+                OnlineAudioImpl::class,
+                OnlineAudioImpl.serializer()
+            )
+            MessageSerializers.registerSerializer(
+                OfflineAudioImpl::class,
+                OfflineAudioImpl.serializer()
+            )
         }
     }
 
@@ -965,14 +973,4 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
             }
         }
     }
-
-    override fun serializePttElem(ptt: Any?): String {
-        if (ptt !is ImMsgBody.Ptt) return ""
-        return ptt.toByteArray(ImMsgBody.Ptt.serializer()).toUHexString()
-    }
-
-    override fun deserializePttElem(ptt: String): Any? {
-        if (ptt.isBlank()) return null
-        return ptt.hexToBytes().loadAs(ImMsgBody.Ptt.serializer())
-    }
 }

+ 37 - 35
mirai-core/src/commonMain/kotlin/contact/FriendImpl.kt

@@ -9,11 +9,7 @@
 
 @file:OptIn(LowLevelApi::class)
 @file:Suppress(
-    "EXPERIMENTAL_API_USAGE",
-    "DEPRECATION_ERROR",
     "NOTHING_TO_INLINE",
-    "INVISIBLE_MEMBER",
-    "INVISIBLE_REFERENCE"
 )
 
 package net.mamoe.mirai.internal.contact
@@ -28,18 +24,19 @@ import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
 import net.mamoe.mirai.event.events.FriendMessagePreSendEvent
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
+import net.mamoe.mirai.internal.message.OfflineAudioImpl
 import net.mamoe.mirai.internal.network.highway.*
 import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x346
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
 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.chat.voice.audioCodec
 import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.internal.utils.C2CPkgMsgParsingCache
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
 import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.message.data.Voice
+import net.mamoe.mirai.message.data.OfflineAudio
 import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.recoverCatchingSuppressed
 import net.mamoe.mirai.utils.toByteArray
@@ -78,10 +75,9 @@ internal class FriendImpl(
             "Friend ${this.id} had already been deleted"
         }
         bot.network.run {
-            FriendList.DelFriend.invoke(bot.client, this@FriendImpl)
-                .sendAndExpect<FriendList.DelFriend.Response>().also {
-                    check(it.isSuccess) { "delete friend failed: ${it.resultCode}" }
-                }
+            FriendList.DelFriend.invoke(bot.client, this@FriendImpl).sendAndExpect().also {
+                check(it.isSuccess) { "delete friend failed: ${it.resultCode}" }
+            }
         }
     }
 
@@ -95,19 +91,13 @@ internal class FriendImpl(
 
     override fun toString(): String = "Friend($id)"
 
-    override suspend fun uploadVoice(resource: ExternalResource): Voice = bot.network.run {
-        val voice = Voice(
-            "${resource.md5.toUHexString("")}.amr",
-            resource.md5,
-            resource.size,
-            resource.voiceCodec,
-            ""
-        )
+    override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio = bot.network.run {
+        var audio: OfflineAudioImpl? = null
         kotlin.runCatching {
             val resp = Highway.uploadResourceBdh(
                 bot = bot,
                 resource = resource,
-                kind = ResourceKind.PRIVATE_VOICE,
+                kind = ResourceKind.PRIVATE_AUDIO,
                 commandId = 26,
                 extendInfo = PttStore.C2C.createC2CPttStoreBDHExt(bot, [email protected], resource)
                     .toByteArray(Cmd0x346.ReqBody.serializer())
@@ -117,14 +107,20 @@ internal class FriendImpl(
             if (c346resp.msgApplyUploadRsp == null) {
                 error("Upload failed")
             }
-            voice.pttInternalInstance = ImMsgBody.Ptt(
-                fileType = 4,
-                srcUin = bot.uin,
-                fileUuid = c346resp.msgApplyUploadRsp.uuid,
+            audio = OfflineAudioImpl(
+                filename = "${resource.md5.toUHexString("")}.amr",
                 fileMd5 = resource.md5,
-                fileName = resource.md5 + ".amr".toByteArray(),
-                fileSize = resource.size.toInt(),
-                boolValid = true,
+                fileSize = resource.size,
+                codec = resource.audioCodec,
+                originalPtt = ImMsgBody.Ptt(
+                    fileType = 4,
+                    srcUin = bot.uin,
+                    fileUuid = c346resp.msgApplyUploadRsp.uuid,
+                    fileMd5 = resource.md5,
+                    fileName = resource.md5 + ".amr".toByteArray(),
+                    fileSize = resource.size.toInt(),
+                    boolValid = true,
+                )
             )
         }.recoverCatchingSuppressed {
             when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect<Any>()) {
@@ -133,24 +129,30 @@ internal class FriendImpl(
                         bot,
                         resp.uploadIpList.zip(resp.uploadPortList),
                         resource.size,
-                        ResourceKind.GROUP_VOICE,
+                        ResourceKind.GROUP_AUDIO,
                         ChannelKind.HTTP
                     ) { ip, port ->
                         Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
                     }
-                    voice.pttInternalInstance = ImMsgBody.Ptt(
-                        fileType = 4,
-                        srcUin = bot.uin,
-                        fileUuid = resp.fileId.toByteArray(),
+                    audio = OfflineAudioImpl(
+                        filename = "${resource.md5.toUHexString("")}.amr",
                         fileMd5 = resource.md5,
-                        fileName = resource.md5 + ".amr".toByteArray(),
-                        fileSize = resource.size.toInt(),
-                        boolValid = true,
+                        fileSize = resource.size,
+                        codec = resource.audioCodec,
+                        originalPtt = ImMsgBody.Ptt(
+                            fileType = 4,
+                            srcUin = bot.uin,
+                            fileUuid = resp.fileId.toByteArray(),
+                            fileMd5 = resource.md5,
+                            fileName = resource.md5 + ".amr".toByteArray(),
+                            fileSize = resource.size.toInt(),
+                            boolValid = true,
+                        )
                     )
                 }
             }
         }.getOrThrow()
 
-        return voice
+        return audio!!
     }
 }

+ 51 - 25
mirai-core/src/commonMain/kotlin/contact/GroupImpl.kt

@@ -23,22 +23,25 @@ import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.contact.announcement.AnnouncementsImpl
 import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
+import net.mamoe.mirai.internal.message.OfflineAudioImpl
 import net.mamoe.mirai.internal.message.OfflineGroupImage
 import net.mamoe.mirai.internal.network.components.BdhSession
 import net.mamoe.mirai.internal.network.handler.NetworkHandler
 import net.mamoe.mirai.internal.network.handler.logger
 import net.mamoe.mirai.internal.network.highway.ChannelKind
 import net.mamoe.mirai.internal.network.highway.Highway
+import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_AUDIO
 import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_IMAGE
-import net.mamoe.mirai.internal.network.highway.ResourceKind.GROUP_VOICE
 import net.mamoe.mirai.internal.network.highway.postPtt
 import net.mamoe.mirai.internal.network.highway.tryServersUpload
 import net.mamoe.mirai.internal.network.protocol.data.proto.Cmd0x388
 import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopEssenceMsgManager
 import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.PttStore
+import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.audioCodec
 import net.mamoe.mirai.internal.network.protocol.packet.chat.voice.voiceCodec
 import net.mamoe.mirai.internal.network.protocol.packet.list.ProfileService
+import net.mamoe.mirai.internal.network.protocol.packet.sendAndExpect
 import net.mamoe.mirai.internal.utils.GroupPkgMsgParsingCache
 import net.mamoe.mirai.internal.utils.RemoteFileImpl
 import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
@@ -204,32 +207,10 @@ internal class GroupImpl(
         }
     }
 
+    @Suppress("OverridingDeprecatedMember", "DEPRECATION")
     override suspend fun uploadVoice(resource: ExternalResource): Voice {
         return bot.network.run {
-            kotlin.runCatching {
-                val (_) = Highway.uploadResourceBdh(
-                    bot = bot,
-                    resource = resource,
-                    kind = GROUP_VOICE,
-                    commandId = 29,
-                    extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource)
-                        .toByteArray(Cmd0x388.ReqBody.serializer()),
-                )
-            }.recoverCatchingSuppressed {
-                when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect<Any>()) {
-                    is PttStore.GroupPttUp.Response.RequireUpload -> {
-                        tryServersUpload(
-                            bot,
-                            resp.uploadIpList.zip(resp.uploadPortList),
-                            resource.size,
-                            GROUP_VOICE,
-                            ChannelKind.HTTP
-                        ) { ip, port ->
-                            Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
-                        }
-                    }
-                }
-            }.getOrThrow()
+            uploadAudioResource(resource)
 
             // val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
             //     ?.msgTryupPttRsp
@@ -243,6 +224,51 @@ internal class GroupImpl(
                 ""
             )
         }
+    }
+
+    private suspend fun uploadAudioResource(resource: ExternalResource) {
+        kotlin.runCatching {
+            val (_) = Highway.uploadResourceBdh(
+                bot = bot,
+                resource = resource,
+                kind = GROUP_AUDIO,
+                commandId = 29,
+                extendInfo = PttStore.GroupPttUp.createTryUpPttPack(bot.id, id, resource)
+                    .toByteArray(Cmd0x388.ReqBody.serializer()),
+            )
+        }.recoverCatchingSuppressed {
+            when (val resp = PttStore.GroupPttUp(bot.client, bot.id, id, resource).sendAndExpect(bot)) {
+                is PttStore.GroupPttUp.Response.RequireUpload -> {
+                    tryServersUpload(
+                        bot,
+                        resp.uploadIpList.zip(resp.uploadPortList),
+                        resource.size,
+                        GROUP_AUDIO,
+                        ChannelKind.HTTP
+                    ) { ip, port ->
+                        Mirai.Http.postPtt(ip, port, resource, resp.uKey, resp.fileKey)
+                    }
+                }
+            }
+        }.getOrThrow()
+    }
+
+    override suspend fun uploadAudio(resource: ExternalResource): OfflineAudio {
+        return bot.network.run {
+            uploadAudioResource(resource)
+
+            // val body = resp?.loadAs(Cmd0x388.RspBody.serializer())
+            //     ?.msgTryupPttRsp
+            //     ?.singleOrNull()?.fileKey ?: error("Group voice highway transfer succeed but failed to find fileKey")
+
+            OfflineAudioImpl(
+                filename = "${resource.md5.toUHexString("")}.amr",
+                fileMd5 = resource.md5,
+                fileSize = resource.size,
+                codec = resource.audioCodec,
+                originalPtt = null,
+            )
+        }
 
     }
 

+ 6 - 3
mirai-core/src/commonMain/kotlin/contact/NormalMemberImpl.kt

@@ -21,8 +21,6 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
-import net.mamoe.mirai.internal.message.OnlineMessageSourceToGroupImpl
-import net.mamoe.mirai.internal.message.OnlineMessageSourceToStrangerImpl
 import net.mamoe.mirai.internal.message.OnlineMessageSourceToTempImpl
 import net.mamoe.mirai.internal.message.createMessageReceipt
 import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement
@@ -171,6 +169,10 @@ internal class NormalMemberImpl constructor(
     }
 
     override suspend fun kick(message: String) {
+        kick(message, false)
+    }
+
+    override suspend fun kick(message: String, block: Boolean) {
         checkBotPermissionHigherThanThis("kick")
         check(group.members[this.id] != null) {
             "Member ${this.id} had already been kicked from group ${group.id}"
@@ -179,7 +181,8 @@ internal class NormalMemberImpl constructor(
             val response: TroopManagement.Kick.Response = TroopManagement.Kick(
                 client = bot.client,
                 member = this@NormalMemberImpl,
-                message = message
+                message = message,
+                ban = block
             ).sendAndExpect()
 
             check(response.success) { "kick failed: ${response.ret}" }

+ 302 - 0
mirai-core/src/commonMain/kotlin/message/OnlineAudioImpl.kt

@@ -0,0 +1,302 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.message
+
+import kotlinx.io.core.toByteArray
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.protobuf.ProtoNumber
+import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.internal.utils.io.ProtoBuf
+import net.mamoe.mirai.internal.utils.io.serialization.loadAs
+import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
+import net.mamoe.mirai.message.data.*
+import net.mamoe.mirai.utils.copy
+import net.mamoe.mirai.utils.map
+
+
+/**
+ * ## Audio Implementation Overview
+ *
+ * ```
+ *                     (api)Audio
+ *                          |
+ *                    /------------------\
+ *         (api)OnlineAudio        (api)OfflineAudio
+ *              |                         |
+ *              |                         |
+ * (core)OnlineAudioImpl      (core)OfflineAudioImpl
+ * ```
+ *
+ * - [OnlineAudioImpl]: 实现从 [ImMsgBody.Ptt] 解析
+ * - [OfflineAudioImpl]: 支持用户手动构造
+ *
+ * ## Equality
+ *
+ * - [OnlineAudio] != [OfflineAudio]
+ *
+ * ## Converting [Audio] to [ImMsgBody.Ptt]
+ *
+ * Always call [Audio.toPtt]
+ */
+internal interface AudioPttSupport : MessageContent { // Audio is sealed in mirai-core-api
+    /**
+     * 原协议数据. 用于在接受到其他用户发送的语音时能按照原样发回.
+     */
+    val originalPtt: ImMsgBody.Ptt?
+}
+
+@Serializable
+internal class AudioExtraData(
+    @ProtoNumber(1) val ptt: ImMsgBody.Ptt?,
+) : ProtoBuf {
+    fun toByteArray(): ByteArray {
+        return Wrapper(CURRENT_VERSION, this).toByteArray(Wrapper.serializer())
+    }
+
+    companion object {
+        @Serializable
+        class Wrapper(
+            @ProtoNumber(1) val version: Int,
+            @ProtoNumber(2) val v1: AudioExtraData? = null,
+        ) : ProtoBuf
+
+        private const val CURRENT_VERSION = 1
+
+
+        fun loadFrom(byteArray: ByteArray?): AudioExtraData? {
+            byteArray ?: return null
+            return kotlin.runCatching {
+                byteArray.loadAs(Wrapper.serializer()).v1 // in this version we only support v1
+            }.getOrNull()
+        }
+    }
+}
+
+internal fun Audio.toPtt(): ImMsgBody.Ptt {
+    if (this is AudioPttSupport) {
+        this.originalPtt?.let { return it }
+    }
+    return ImMsgBody.Ptt(
+        fileName = this.filename.toByteArray(),
+        fileMd5 = this.fileMd5,
+        boolValid = true,
+        fileSize = this.fileSize.toInt(),
+        fileType = 4,
+        pbReserve = byteArrayOf(0),
+        format = this.codec.id
+    )
+}
+
+@SerialName(OnlineAudio.SERIAL_NAME)
+@Serializable(OnlineAudioImpl.Serializer::class)
+internal class OnlineAudioImpl(
+    override val filename: String,
+    override val fileMd5: ByteArray,
+    override val fileSize: Long,
+    override val codec: AudioCodec,
+    url: String,
+    override val length: Long,
+    override val originalPtt: ImMsgBody.Ptt?,
+) : OnlineAudio, AudioPttSupport {
+    private val _url = refineUrl(url)
+
+    override val extraData: ByteArray? by lazy {
+        AudioExtraData(originalPtt).toByteArray()
+    }
+
+    override val urlForDownload: String
+        get() = _url.takeIf { it.isNotBlank() }
+            ?: throw UnsupportedOperationException("Could not fetch URL for audio $filename")
+
+    private val _stringValue: String by lazy { "[mirai:audio:${filename}]" }
+    override fun toString(): String = _stringValue
+
+    @Suppress("DuplicatedCode")
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as OnlineAudioImpl
+
+        if (filename != other.filename) return false
+        if (!fileMd5.contentEquals(other.fileMd5)) return false
+        if (fileSize != other.fileSize) return false
+        if (_url != other._url) return false
+        if (codec != other.codec) return false
+        if (length != other.length) return false
+        if (originalPtt != other.originalPtt) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = super.hashCode()
+        result = 31 * result + filename.hashCode()
+        result = 31 * result + fileMd5.contentHashCode()
+        result = 31 * result + fileSize.hashCode()
+        result = 31 * result + _url.hashCode()
+        result = 31 * result + codec.hashCode()
+        result = 31 * result + length.hashCode()
+        result = 31 * result + originalPtt.hashCode()
+        return result
+    }
+
+
+    companion object {
+        fun refineUrl(url: String) = when {
+            url.isBlank() -> ""
+            url.startsWith("http") -> url
+            url.startsWith("/") -> "$DOWNLOAD_URL$url"
+            else -> "$DOWNLOAD_URL/$url"
+        }
+
+        @Suppress("HttpUrlsUsage")
+        const val DOWNLOAD_URL = "http://grouptalk.c2c.qq.com"
+    }
+
+    object Serializer : KSerializer<OnlineAudioImpl> by Surrogate.serializer().map(
+        resultantDescriptor = Surrogate.serializer().descriptor.copy(OnlineAudio.SERIAL_NAME),
+        deserialize = {
+            OnlineAudioImpl(
+                filename = filename,
+                fileMd5 = fileMd5,
+                fileSize = fileSize,
+                url = urlForDownload,
+                codec = codec,
+                length = length,
+                originalPtt = AudioExtraData.loadFrom(extraData)?.ptt
+            )
+        },
+        serialize = {
+            Surrogate(
+                filename = filename,
+                fileMd5 = fileMd5,
+                fileSize = fileSize,
+                urlForDownload = urlForDownload,
+                codec = codec,
+                length = length,
+                extraData = extraData
+            )
+        }
+    ) {
+        @Serializable
+        @SerialName(OnlineAudio.SERIAL_NAME)
+        private class Surrogate(
+            override val filename: String,
+            override val fileMd5: ByteArray,
+            override val fileSize: Long,
+            override val codec: AudioCodec,
+            override val length: Long,
+            override val extraData: ByteArray?,
+            override val urlForDownload: String,
+        ) : OnlineAudio {
+            override fun toString(): String {
+                return "Surrogate(filename='$filename', fileMd5=${fileMd5.contentToString()}, fileSize=$fileSize, codec=$codec, length=$length, extraData=${extraData.contentToString()}, urlForDownload='$urlForDownload')"
+            }
+        }
+    }
+}
+
+@SerialName(OfflineAudio.SERIAL_NAME)
+@Serializable(OfflineAudioImpl.Serializer::class)
+internal class OfflineAudioImpl(
+    override val filename: String,
+    override val fileMd5: ByteArray,
+    override val fileSize: Long,
+    override val codec: AudioCodec,
+    override val originalPtt: ImMsgBody.Ptt?,
+) : OfflineAudio, AudioPttSupport {
+    constructor(
+        filename: String,
+        fileMd5: ByteArray,
+        fileSize: Long,
+        codec: AudioCodec,
+        extraData: ByteArray?,
+    ) : this(filename, fileMd5, fileSize, codec, AudioExtraData.loadFrom(extraData)?.ptt)
+
+    override val extraData: ByteArray? by lazy {
+        AudioExtraData(originalPtt).toByteArray()
+    }
+
+    private val _stringValue: String by lazy { "[mirai:audio:${filename}]" }
+    override fun toString(): String = _stringValue
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as OfflineAudioImpl
+
+        if (filename != other.filename) return false
+        if (!fileMd5.contentEquals(other.fileMd5)) return false
+        if (fileSize != other.fileSize) return false
+        if (codec != other.codec) return false
+        if (originalPtt != other.originalPtt) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = filename.hashCode()
+        result = 31 * result + fileMd5.contentHashCode()
+        result = 31 * result + fileSize.hashCode()
+        result = 31 * result + codec.hashCode()
+        result = 31 * result + originalPtt.hashCode()
+        return result
+    }
+
+    object Serializer : KSerializer<OfflineAudioImpl> by Surrogate.serializer().map(
+        resultantDescriptor = Surrogate.serializer().descriptor.copy(OfflineAudio.SERIAL_NAME),
+        deserialize = {
+            OfflineAudioImpl(
+                filename = filename,
+                fileMd5 = fileMd5,
+                fileSize = fileSize,
+                codec = codec,
+                extraData = extraData,
+            )
+        },
+        serialize = {
+            Surrogate(
+                filename = filename,
+                fileMd5 = fileMd5,
+                fileSize = fileSize,
+                codec = codec,
+                extraData = extraData,
+            )
+        }
+    ) {
+        @Serializable
+        @SerialName(OfflineAudio.SERIAL_NAME)
+        private class Surrogate(
+            override val filename: String,
+            override val fileMd5: ByteArray,
+            override val fileSize: Long,
+            override val codec: AudioCodec,
+            override val extraData: ByteArray?,
+        ) : OfflineAudio {
+            override fun toString(): String {
+                return "OfflineAudio(filename='$filename', fileMd5=${fileMd5.contentToString()}, fileSize=$fileSize, codec=$codec, extraData=${extraData.contentToString()})"
+            }
+        }
+    }
+}
+
+@PublishedApi
+internal class OfflineAudioFactoryImpl : OfflineAudio.Factory {
+    override fun create(
+        filename: String,
+        fileMd5: ByteArray,
+        fileSize: Long,
+        codec: AudioCodec,
+        extraData: ByteArray?
+    ): OfflineAudio = OfflineAudioImpl(filename, fileMd5, fileSize, codec, extraData)
+}

+ 12 - 13
mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt

@@ -18,15 +18,12 @@ import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
 import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight
 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.message.ReceiveMessageTransformer.toAudio
 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.encodeToString
-import net.mamoe.mirai.utils.read
-import net.mamoe.mirai.utils.toUHexString
-import net.mamoe.mirai.utils.unzip
+import net.mamoe.mirai.utils.*
 
 /**
  * 只在手动构造 [OfflineMessageSource] 时调用
@@ -91,7 +88,7 @@ private fun List<MsgComm.Msg>.toMessageChain(
     joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder)
 
     for (msg in messageList) {
-        msg.msgBody.richText.ptt?.toVoice()?.let { builder.add(it) }
+        msg.msgBody.richText.ptt?.toAudio()?.let { builder.add(it) }
     }
 
     return builder.build().cleanupRubbishMessageElements()
@@ -516,11 +513,13 @@ internal object ReceiveMessageTransformer {
         }
     }
 
-    fun ImMsgBody.Ptt.toVoice() = Voice(
-        kotlinx.io.core.String(fileName),
-        fileMd5,
-        fileSize.toLong(),
-        format,
-        kotlinx.io.core.String(downPara)
-    ).also { it.pttInternalInstance = this }
+    fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl(
+        filename = fileName.encodeToString(),
+        fileMd5 = fileMd5,
+        fileSize = fileSize.toLongUnsigned(),
+        codec = AudioCodec.fromId(format),
+        url = downPara.encodeToString(),
+        length = time.toLongUnsigned(),
+        originalPtt = this,
+    )
 }

+ 5 - 2
mirai-core/src/commonMain/kotlin/message/imagesImpl.kt

@@ -57,6 +57,9 @@ internal class OnlineGroupImageImpl(
 
 }
 
+private val imageLogger: MiraiLogger by lazy { MiraiLogger.Factory.create(Image::class) }
+internal val Image.Key.logger get() = imageLogger
+
 @Serializable(with = OnlineFriendImageImpl.Serializer::class)
 internal class OnlineFriendImageImpl(
     internal val delegate: ImMsgBody.NotOnlineImage,
@@ -70,7 +73,7 @@ OnlineFriendImage() {
             ?: kotlin.run {
                 if (delegate.picMd5.size == 16) generateImageId(delegate.picMd5, imageType)
                 else {
-                    MiraiLogger.TopLevel.warning(
+                    Image.logger.warning(
                         contextualBugReportException(
                             "Failed to compute friend imageId: resId=${delegate.resId}",
                             delegate._miraiContentToString(),
@@ -115,7 +118,7 @@ internal fun getImageType(id: Int): String {
         2001, 3 -> "png"
         else -> {
             if (UNKNOWN_IMAGE_TYPE_PROMPT_ENABLED) {
-                MiraiLogger.TopLevel.debug(
+                Image.logger.debug(
                     "Unknown image id: $id. Stacktrace:",
                     Exception()
                 )

+ 1 - 1
mirai-core/src/commonMain/kotlin/network/components/PacketCodec.kt

@@ -45,7 +45,7 @@ internal interface PacketCodec {
         val PACKET_DEBUG = systemProp("mirai.network.packet.logger", false)
 
         internal val PacketLogger: MiraiLoggerWithSwitch by lazy {
-            MiraiLogger.create("Packet").withSwitch(PACKET_DEBUG)
+            MiraiLogger.Factory.create(PacketCodec::class, "Packet").withSwitch(PACKET_DEBUG)
         }
     }
 }

+ 3 - 3
mirai-core/src/commonMain/kotlin/network/components/ServerList.kt

@@ -13,8 +13,8 @@ import kotlinx.serialization.Serializable
 import net.mamoe.mirai.internal.network.component.ComponentKey
 import net.mamoe.mirai.internal.network.components.ServerList.Companion.DEFAULT_SERVER_LIST
 import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.TestOnly
 import net.mamoe.mirai.utils.info
-import org.jetbrains.annotations.TestOnly
 import java.net.InetSocketAddress
 import java.util.*
 
@@ -105,10 +105,10 @@ internal class ServerListImpl(
     initial: Collection<ServerAddress> = emptyList()
 ) : ServerList {
     @TestOnly
-    constructor(initial: Collection<ServerAddress>) : this(MiraiLogger.TopLevel, initial)
+    constructor(initial: Collection<ServerAddress>) : this(MiraiLogger.Factory.create(ServerListImpl::class), initial)
 
     @TestOnly
-    constructor() : this(MiraiLogger.TopLevel)
+    constructor() : this(MiraiLogger.Factory.create(ServerListImpl::class))
 
     @Volatile
     private var preferred: Set<ServerAddress> = DEFAULT_SERVER_LIST

+ 2 - 1
mirai-core/src/commonMain/kotlin/network/handler/NetworkHandlerSupport.kt

@@ -23,6 +23,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.IncomingPacket
 import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.internal.utils.SingleEntrantLock
 import net.mamoe.mirai.internal.utils.fromMiraiLogger
+import net.mamoe.mirai.internal.utils.subLogger
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.Either.Companion.fold
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -56,7 +57,7 @@ internal abstract class NetworkHandlerSupport(
     }
 
     protected val packetLogger: MiraiLogger by lazy {
-        MiraiLogger.create(context.logger.identity + ".debug").withSwitch(PacketCodec.PACKET_DEBUG)
+        context.logger.subLogger("NetworkDebug").withSwitch(PacketCodec.PACKET_DEBUG)
     }
 
     ///////////////////////////////////////////////////////////////////////////

+ 4 - 4
mirai-core/src/commonMain/kotlin/network/handler/state/LoggingStateObserver.kt

@@ -84,14 +84,14 @@ internal class LoggingStateObserver(
             return when (ENABLED) {
                 "full" -> {
                     SafeStateObserver(
-                        LoggingStateObserver(MiraiLogger.create("States"), true),
-                        MiraiLogger.create("LoggingStateObserver errors")
+                        LoggingStateObserver(MiraiLogger.Factory.create(LoggingStateObserver::class, "States"), true),
+                        MiraiLogger.Factory.create(LoggingStateObserver::class, "LoggingStateObserver errors")
                     )
                 }
                 "on", "true" -> {
                     SafeStateObserver(
-                        LoggingStateObserver(MiraiLogger.create("States"), false),
-                        MiraiLogger.create("LoggingStateObserver errors")
+                        LoggingStateObserver(MiraiLogger.Factory.create(LoggingStateObserver::class, "States"), false),
+                        MiraiLogger.Factory.create(LoggingStateObserver::class, "LoggingStateObserver errors")
                     )
                 }
                 else -> null

+ 2 - 2
mirai-core/src/commonMain/kotlin/network/highway/Highway.kt

@@ -122,8 +122,8 @@ internal enum class ResourceKind(
 ) {
     PRIVATE_IMAGE("private image"),
     GROUP_IMAGE("group image"),
-    PRIVATE_VOICE("private voice"),
-    GROUP_VOICE("group voice"),
+    PRIVATE_AUDIO("private audio"),
+    GROUP_AUDIO("group audio"),
 
     GROUP_FILE("group file"),
 

+ 63 - 1
mirai-core/src/commonMain/kotlin/network/protocol/data/proto/Msg.kt

@@ -765,7 +765,69 @@ internal class ImMsgBody : ProtoBuf {
         @ProtoNumber(30) @JvmField val pbReserve: ByteArray = EMPTY_BYTE_ARRAY,
         @ProtoNumber(31) @JvmField val bytesPttUrls: List<ByteArray> = emptyList(),
         @ProtoNumber(32) @JvmField val downloadFlag: Int = 0,
-    ) : ProtoBuf
+    ) : ProtoBuf {
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (javaClass != other?.javaClass) return false
+
+            other as Ptt
+
+            if (fileType != other.fileType) return false
+            if (srcUin != other.srcUin) return false
+            if (!fileUuid.contentEquals(other.fileUuid)) return false
+            if (!fileMd5.contentEquals(other.fileMd5)) return false
+            if (!fileName.contentEquals(other.fileName)) return false
+            if (fileSize != other.fileSize) return false
+            if (!reserve.contentEquals(other.reserve)) return false
+            if (fileId != other.fileId) return false
+            if (serverIp != other.serverIp) return false
+            if (serverPort != other.serverPort) return false
+            if (boolValid != other.boolValid) return false
+            if (!signature.contentEquals(other.signature)) return false
+            if (!shortcut.contentEquals(other.shortcut)) return false
+            if (!fileKey.contentEquals(other.fileKey)) return false
+            if (magicPttIndex != other.magicPttIndex) return false
+            if (voiceSwitch != other.voiceSwitch) return false
+            if (!pttUrl.contentEquals(other.pttUrl)) return false
+            if (!groupFileKey.contentEquals(other.groupFileKey)) return false
+            if (time != other.time) return false
+            if (!downPara.contentEquals(other.downPara)) return false
+            if (format != other.format) return false
+            if (!pbReserve.contentEquals(other.pbReserve)) return false
+            if (bytesPttUrls != other.bytesPttUrls) return false
+            if (downloadFlag != other.downloadFlag) return false
+
+            return true
+        }
+
+        override fun hashCode(): Int {
+            var result = fileType
+            result = 31 * result + srcUin.hashCode()
+            result = 31 * result + fileUuid.contentHashCode()
+            result = 31 * result + fileMd5.contentHashCode()
+            result = 31 * result + fileName.contentHashCode()
+            result = 31 * result + fileSize
+            result = 31 * result + reserve.contentHashCode()
+            result = 31 * result + fileId
+            result = 31 * result + serverIp
+            result = 31 * result + serverPort
+            result = 31 * result + boolValid.hashCode()
+            result = 31 * result + signature.contentHashCode()
+            result = 31 * result + shortcut.contentHashCode()
+            result = 31 * result + fileKey.contentHashCode()
+            result = 31 * result + magicPttIndex
+            result = 31 * result + voiceSwitch
+            result = 31 * result + pttUrl.contentHashCode()
+            result = 31 * result + groupFileKey.contentHashCode()
+            result = 31 * result + time
+            result = 31 * result + downPara.contentHashCode()
+            result = 31 * result + format
+            result = 31 * result + pbReserve.contentHashCode()
+            result = 31 * result + bytesPttUrls.hashCode()
+            result = 31 * result + downloadFlag
+            return result
+        }
+    }
 
     @Serializable
     internal class PubAccInfo(

+ 3 - 2
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt

@@ -192,7 +192,8 @@ internal class TroopManagement {
         operator fun invoke(
             client: QQAndroidClient,
             member: Member,
-            message: String
+            message: String,
+            ban: Boolean
         ) = buildOutgoingUniPacket(client) {
             writeProtoBuf(
                 OidbSso.OIDBSSOPkg.serializer(),
@@ -206,7 +207,7 @@ internal class TroopManagement {
                             Oidb0x8a0.KickMemberInfo(
                                 optUint32Operate = 5,
                                 optUint64MemberUin = member.id,
-                                optUint32Flag = 0
+                                optUint32Flag = if (ban) 1 else 0 //1为拉黑
                             )
                         ),
                         kickMsg = message.toByteArray()

+ 9 - 4
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt

@@ -145,7 +145,8 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
         return response
     }
 
-    internal fun PttMessage.toPtt() = run {
+    // old Voice
+    private fun PttMessage.toPtt() = run {
         (this.pttInternalInstance as? ImMsgBody.Ptt)?.let { return it }
         ImMsgBody.Ptt(
             fileName = fileName.toByteArray(),
@@ -155,8 +156,9 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
             fileType = 4,
             pbReserve = byteArrayOf(0),
             format = let {
+                @Suppress("DEPRECATION")
                 if (it is Voice) {
-                    it.codec
+                    it._codec
                 } else {
                     0
                 }
@@ -244,7 +246,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
                 ImMsgBody.MsgBody(
                     richText = ImMsgBody.RichText(
                         elems = subChain.toRichTextElems(messageTarget = targetFriend, withGeneralFlags = true),
-                        ptt = subChain[PttMessage]?.toPtt(),
+                        ptt = subChain.findPtt(),
                     )
                 )
             },
@@ -361,7 +363,7 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
                 ImMsgBody.MsgBody(
                     richText = ImMsgBody.RichText(
                         elems = subChain.toRichTextElems(messageTarget = targetGroup, withGeneralFlags = true),
-                        ptt = subChain[PttMessage]?.toPtt()
+                        ptt = subChain.findPtt()
 
                     )
                 )
@@ -409,6 +411,9 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
             doFragmented = fragmented
         )
     }
+
+    private fun MessageChain.findPtt() =
+        findIsInstance<Audio>()?.toPtt() ?: this[PttMessage]?.toPtt()
     /*
     = buildOutgoingUniPacket(client) {
         ///writeFully("0A 08 0A 06 08 89 FC A6 8C 0B 12 06 08 01 10 00 18 00 1A 1F 0A 1D 12 08 0A 06 0A 04 F0 9F 92 A9 12 11 AA 02 0E 88 01 00 9A 01 08 78 00 F8 01 00 C8 02 00 20 9B 7A 28 F4 CA 9B B8 03 32 34 08 92 C2 C4 F1 05 10 92 C2 C4 F1 05 18 E6 ED B9 C3 02 20 89 FE BE A4 06 28 89 84 F9 A2 06 48 DE 8C EA E5 0E 58 D9 BD BB A0 09 60 1D 68 92 C2 C4 F1 05 70 00 40 01".hexToBytes())

+ 7 - 4
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/voice/PttStore.kt

@@ -22,18 +22,21 @@ import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
 import net.mamoe.mirai.internal.utils.toIpV4AddressString
+import net.mamoe.mirai.message.data.AudioCodec
 import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.utils.ExternalResource
 import net.mamoe.mirai.utils.encodeToString
 import net.mamoe.mirai.utils.toUHexString
 
-internal val ExternalResource.voiceCodec: Int
+internal inline val ExternalResource.voiceCodec: Int get() = audioCodec.id
+
+internal val ExternalResource.audioCodec: AudioCodec
     get() {
         return when (formatName) {
             // 实际上 amr 是 0, 但用 1 也可以发. 为了避免 silk 错被以 amr 发送导致降音质就都用 1
-            "amr" -> 1  // amr
-            "silk" -> 1  // silk V3
-            else -> 1     // use amr by default
+            "amr" -> AudioCodec.SILK
+            "silk" -> AudioCodec.SILK
+            else -> AudioCodec.AMR     // use amr by default
         }
     }
 

+ 4 - 52
mirai-core/src/commonMain/kotlin/utils/SubLogger.kt

@@ -11,7 +11,8 @@ package net.mamoe.mirai.internal.utils
 
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineExceptionHandler
-import net.mamoe.mirai.utils.*
+import net.mamoe.mirai.utils.MiraiLogger
+import net.mamoe.mirai.utils.coroutineName
 
 
 internal fun CoroutineExceptionHandler.Key.fromMiraiLogger(
@@ -25,54 +26,5 @@ internal fun CoroutineExceptionHandler.Key.fromMiraiLogger(
     }
 }
 
-internal fun MiraiLogger.subLogger(name: String): MiraiLogger {
-    return SubLogger(name, this)
-}
-
-private class SubLogger(
-    private val name: String,
-    private val main: MiraiLogger,
-) : MiraiLoggerPlatformBase() {
-    override val identity: String? get() = main.identity
-    override val isEnabled: Boolean get() = main.isEnabled
-
-    override fun verbose0(message: String?, e: Throwable?) {
-        if (message != null) {
-            main.verbose({ "[$name] $message" }, e)
-        } else {
-            main.verbose(null, e)
-        }
-    }
-
-    override fun debug0(message: String?, e: Throwable?) {
-        if (message != null) {
-            main.debug({ "[$name] $message" }, e)
-        } else {
-            main.debug(null, e)
-        }
-    }
-
-    override fun info0(message: String?, e: Throwable?) {
-        if (message != null) {
-            main.info({ "[$name] $message" }, e)
-        } else {
-            main.info(null, e)
-        }
-    }
-
-    override fun warning0(message: String?, e: Throwable?) {
-        if (message != null) {
-            main.warning({ "[$name] $message" }, e)
-        } else {
-            main.warning(null, e)
-        }
-    }
-
-    override fun error0(message: String?, e: Throwable?) {
-        if (message != null) {
-            main.error({ "[$name] $message" }, e)
-        } else {
-            main.error(null, e)
-        }
-    }
-}
+@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
+internal fun MiraiLogger.subLogger(name: String): MiraiLogger = subLoggerImpl(this, name)

+ 2 - 1
mirai-core/src/commonMain/kotlin/utils/contentToString.kt

@@ -12,6 +12,7 @@
 package net.mamoe.mirai.internal.utils
 
 import kotlinx.serialization.Transient
+import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.utils.MiraiLogger
 import net.mamoe.mirai.utils.debug
 import net.mamoe.mirai.utils.toUHexString
@@ -32,7 +33,7 @@ private fun <T> Sequence<T>.joinToStringPrefixed(prefix: String, transform: (T)
     return this.joinToString(prefix = "$prefix$indent", separator = "\n$prefix$indent", transform = transform)
 }
 
-private val SoutvLogger: MiraiLogger by lazy { MiraiLogger.create("soutv") }
+private val SoutvLogger: MiraiLogger by lazy { MiraiLogger.Factory.create(IMirai::class, "soutv") }
 internal fun Any?.soutv(name: String = "unnamed") {
     @Suppress("DEPRECATION")
     SoutvLogger.debug { "$name = ${this._miraiContentToString()}" }

+ 10 - 0
mirai-core/src/commonMain/resources/META-INF/services/net.mamoe.mirai.message.data.OfflineAudio.Factory

@@ -0,0 +1,10 @@
+#
+# Copyright 2019-2021 Mamoe Technologies and contributors.
+#
+# 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+# Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+#
+# https://github.com/mamoe/mirai/blob/dev/LICENSE
+#
+
+net.mamoe.mirai.internal.message.OfflineAudioFactoryImpl

+ 75 - 0
mirai-core/src/commonTest/kotlin/message/AudioTest.kt

@@ -0,0 +1,75 @@
+/*
+ * Copyright 2019-2021 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
+ */
+
+package net.mamoe.mirai.internal.message
+
+import net.mamoe.mirai.internal.message.OnlineAudioImpl.Companion.DOWNLOAD_URL
+import net.mamoe.mirai.internal.message.OnlineAudioImpl.Companion.refineUrl
+import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.internal.test.AbstractTest
+import net.mamoe.mirai.message.data.AudioCodec
+import net.mamoe.mirai.message.data.OfflineAudio
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+internal class AudioTest : AbstractTest() {
+
+    @Test
+    fun `test factory`() {
+        assertEquals(
+            OfflineAudio("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()),
+            OfflineAudio("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf())
+        )
+    }
+
+    @Test
+    fun `invalid extraData is refreshed`() {
+        assertContentEquals(
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null).extraData,
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2, 3)).extraData,
+        )
+    }
+
+    @Test
+    fun `test equality`() {
+        assertEquals(
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null),
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null)
+        )
+        assertEquals(
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null),
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null)
+        )
+        assertEquals(
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, originalPtt = null),
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, originalPtt = null)
+        )
+        assertEquals(
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf()),
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf())
+        )
+        assertEquals(
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, ImMsgBody.Ptt(srcUin = 2)),
+            OfflineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, ImMsgBody.Ptt(srcUin = 2))
+        )
+    }
+
+    @Test
+    fun `test refineUrl`() {
+        assertFalse { DOWNLOAD_URL.endsWith("/") }
+
+        assertEquals("", refineUrl(""))
+        assertEquals("$DOWNLOAD_URL/test", refineUrl("/test"))
+        assertEquals("$DOWNLOAD_URL/test", refineUrl("test"))
+        assertEquals("https://custom.com", refineUrl("https://custom.com"))
+        assertEquals("http://localhost", refineUrl("http://localhost"))
+    }
+}

+ 4 - 1
mirai-core/src/commonTest/kotlin/network/component/EventDispatcherTest.kt

@@ -26,7 +26,10 @@ import org.junit.jupiter.api.Test
 internal class EventDispatcherTest : AbstractTest() {
     private class Ev : AbstractEvent()
 
-    private val dispatcher = TestEventDispatcherImpl(SupervisorJob(), MiraiLogger.create("EventDispatcherTest"))
+    private val dispatcher = TestEventDispatcherImpl(
+        SupervisorJob(),
+        MiraiLogger.Factory.create(EventDispatcherTest::class)
+    )
 
     @Test
     fun `can broadcast`() = runBlockingUnit {

+ 4 - 3
mirai-core/src/commonTest/kotlin/network/framework/AbstractMockNetworkHandlerTest.kt

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai.internal.network.framework
 
+import net.mamoe.mirai.Bot
 import net.mamoe.mirai.internal.MockBot
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
@@ -39,7 +40,7 @@ internal abstract class AbstractMockNetworkHandlerTest : AbstractNetworkHandlerT
         nhProvider = { createNetworkHandler() }
         additionalComponentsProvider = { [email protected] }
     }
-    protected val logger = MiraiLogger.create("test")
+    protected val logger = MiraiLogger.Factory.create(Bot::class, "test")
     protected val components = ConcurrentComponentStorage().apply {
         set(SsoProcessor, TestSsoProcessor(bot))
         set(
@@ -49,8 +50,8 @@ internal abstract class AbstractMockNetworkHandlerTest : AbstractNetworkHandlerT
         set(
             StateObserver,
             SafeStateObserver(
-                LoggingStateObserver(MiraiLogger.create("States")),
-                MiraiLogger.create("StateObserver errors")
+                LoggingStateObserver(MiraiLogger.Factory.create(LoggingStateObserver::class, "States")),
+                MiraiLogger.Factory.create(SafeStateObserver::class, "StateObserver errors")
             )
         )
     }

+ 1 - 1
mirai-core/src/commonTest/kotlin/network/framework/AbstractRealNetworkHandlerTest.kt

@@ -53,7 +53,7 @@ internal sealed class AbstractRealNetworkHandlerTest<H : NetworkHandler> : Abstr
         }
     }
 
-    open val networkLogger = MiraiLogger.create("network")
+    open val networkLogger = MiraiLogger.Factory.create(NetworkHandler::class, "network")
 
     sealed class NHEvent {
         object Login : NHEvent()

+ 3 - 3
mirai-core/src/commonTest/kotlin/network/framework/TestNetworkHandlerContext.kt

@@ -24,14 +24,14 @@ import net.mamoe.mirai.utils.MiraiLogger
 
 internal class TestNetworkHandlerContext(
     override val bot: QQAndroidBot = MockBot(),
-    override val logger: MiraiLogger = MiraiLogger.create("Test"),
+    override val logger: MiraiLogger = MiraiLogger.Factory.create(TestNetworkHandlerContext::class, "Test"),
     components: ComponentStorage = ConcurrentComponentStorage().apply {
         set(SsoProcessor, SsoProcessorImpl(SsoProcessorContextImpl(bot)))
         set(
             StateObserver,
             SafeStateObserver(
-                LoggingStateObserver(MiraiLogger.create("States")),
-                MiraiLogger.create("StateObserver errors")
+                LoggingStateObserver(MiraiLogger.Factory.create(LoggingStateObserver::class, "States")),
+                MiraiLogger.Factory.create(LoggingStateObserver::class, "StateObserver errors")
             )
         )
     }

+ 1 - 1
mirai-core/src/commonTest/kotlin/network/handler/KeepAliveNetworkHandlerSelectorTest.kt

@@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger
 import kotlin.test.*
 import kotlin.time.Duration
 
-internal val selectorLogger = MiraiLogger.create("selector")
+internal val selectorLogger = MiraiLogger.Factory.create(TestSelector::class, "selector")
 
 internal class TestSelector<H : NetworkHandler> :
     AbstractKeepAliveNetworkHandlerSelector<H> {

+ 10 - 3
mirai-core/src/commonTest/kotlin/test/initPlatform.common.kt

@@ -12,6 +12,7 @@ package net.mamoe.mirai.internal.test
 import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.internal.network.framework.SynchronizedStdoutLogger
 import net.mamoe.mirai.utils.MiraiLogger
+import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.Timeout
 import java.util.concurrent.TimeUnit
@@ -26,9 +27,7 @@ abstract class AbstractTest {
     init {
         initPlatform()
 
-        MiraiLogger.setDefaultLoggerCreator {
-            SynchronizedStdoutLogger(it)
-        }
+        restoreLoggerFactory()
 
         System.setProperty("mirai.network.packet.logger", "true")
         System.setProperty("mirai.network.state.observer.logging", "true")
@@ -38,6 +37,14 @@ abstract class AbstractTest {
 
     }
 
+    @AfterEach
+    protected fun restoreLoggerFactory() {
+        @Suppress("DEPRECATION")
+        MiraiLogger.setDefaultLoggerCreator {
+            SynchronizedStdoutLogger(it)
+        }
+    }
+
     companion object {
         init {
             Exception() // create a exception to load relevant classes to estimate invocation time of test cases more accurately.

+ 6 - 5
mirai-core/src/commonTest/kotlin/test/printing.kt

@@ -1,10 +1,10 @@
 /*
- * 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.
+ * 此源代码的使用受 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
+ * https://github.com/mamoe/mirai/blob/dev/LICENSE
  */
 
 @file:Suppress("NOTHING_TO_INLINE")
@@ -14,12 +14,13 @@ package test
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.Input
 import kotlinx.io.core.readAvailable
+import net.mamoe.mirai.IMirai
 import net.mamoe.mirai.utils.*
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 
 
-val DebugLogger: MiraiLogger = MiraiLogger.create("Packet Debug")
+val DebugLogger: MiraiLogger = MiraiLogger.Factory.create(IMirai::class, "Packet Debug")
 
 internal inline fun ByteArray.debugPrintThis(name: String): ByteArray {
     DebugLogger.debug(name + "=" + this.toUHexString())

+ 86 - 26
mirai-core/src/jvmTest/kotlin/message/data/MessageSerializationTest.kt

@@ -12,13 +12,12 @@ package net.mamoe.mirai.internal.message.data
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.Polymorphic
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.*
 import kotlinx.serialization.serializer
 import net.mamoe.mirai.Mirai
 import net.mamoe.mirai.internal.message.FileMessageImpl
 import net.mamoe.mirai.internal.message.MarketFaceImpl
+import net.mamoe.mirai.internal.message.OnlineAudioImpl
 import net.mamoe.mirai.internal.message.UnsupportedMessageImpl
 import net.mamoe.mirai.internal.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.internal.utils._miraiContentToString
@@ -220,39 +219,100 @@ internal class MessageSerializationTest {
 
     @Serializable
     data class V(
-        val msg: Voice
+        val msg: Audio
+    )
+
+    @Serializable
+    data class AudioTestStandard(
+        val online: OnlineAudio,
+        val offline: OfflineAudio,
+        val onlineAsRec: Audio,
+        val offlineAsRec: Audio,
     )
 
     @Test
-    fun `test Voice serialization`() {
-        val v = V(Voice("4517", byteArrayOf(14), 50, 3, "https://github.com"))
-        println(v.serialize(V.serializer()))
-        assertEquals(
-            v.serialize(V.serializer()),
-            v.serialize(V.serializer())
-                .deserialize(V.serializer())
-                .serialize(V.serializer())
+    fun `test Audio standard`() {
+        val origin = AudioTestStandard(
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null),
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null),
+
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, null),
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, null),
         )
+
+        assertEquals(
+            AudioCodec.SILK.id,
+            format.encodeToJsonElement(origin).jsonObject["offline"]!!.jsonObject["codec"]!!.jsonPrimitive.content.toInt()
+        ) // use custom serializer
+
+        assertEquals(
+            AudioCodec.SILK.id,
+            format.encodeToJsonElement(origin).jsonObject["online"]!!.jsonObject["codec"]!!.jsonPrimitive.content.toInt()
+        ) // use custom serializer
+
         assertEquals(
-            v,
-            v.serialize(V.serializer()).deserialize(V.serializer())
+            "OnlineAudio",
+            format.encodeToJsonElement(origin).jsonObject["online"]!!.jsonObject["type"]!!.jsonPrimitive.content
         )
-        v.msg.pttInternalInstance = ImMsgBody.Ptt(
-            srcUin = 1234567890,
-            fileMd5 = byteArrayOf(14, 81, 37, 14),
-            boolValid = true,
-            format = 90,
+        assertEquals(
+            "OfflineAudio",
+            format.encodeToJsonElement(origin).jsonObject["offline"]!!.jsonObject["type"]!!.jsonPrimitive.content
         )
-        println(v.serialize(V.serializer()))
+
         assertEquals(
-            v.serialize(V.serializer()),
-            v.serialize(V.serializer())
-                .deserialize(V.serializer())
-                .serialize(V.serializer())
+            "OnlineAudio",
+            format.encodeToJsonElement(origin).jsonObject["onlineAsRec"]!!.jsonObject["type"]!!.jsonPrimitive.content
         )
         assertEquals(
-            v,
-            v.serialize(V.serializer()).deserialize(V.serializer())
+            "OfflineAudio",
+            format.encodeToJsonElement(origin).jsonObject["offlineAsRec"]!!.jsonObject["type"]!!.jsonPrimitive.content
+        )
+
+        val result = origin.serialize().deserialize<AudioTestStandard>()
+
+        assertEquals(origin.online::class, result.online::class)
+        assertEquals(origin.offline::class, result.offline::class)
+        assertEquals(origin.onlineAsRec::class, result.onlineAsRec::class)
+        assertEquals(origin.offlineAsRec::class, result.offlineAsRec::class)
+
+        assertEquals(origin.online, result.online)
+        assertEquals(origin.offline, result.offline)
+        assertEquals(origin.onlineAsRec, result.onlineAsRec)
+        assertEquals(origin.offlineAsRec, result.offlineAsRec)
+
+        assertEquals(origin, result)
+    }
+
+    @Serializable
+    data class AudioTestWithPtt(
+        val online: OnlineAudio,
+        val offline: OfflineAudio,
+        val onlineAsRec: Audio,
+        val offlineAsRec: Audio,
+    )
+
+    @Test
+    fun `test Audio with ptt`() {
+        val origin = AudioTestWithPtt(
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, ImMsgBody.Ptt(1)),
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2)),
+
+            OnlineAudioImpl("name", byteArrayOf(), 1, AudioCodec.SILK, "url", 2, ImMsgBody.Ptt(1)),
+            OfflineAudio("test", byteArrayOf(), 1, AudioCodec.SILK, byteArrayOf(1, 2)),
         )
+
+        val result = origin.serialize().deserialize<AudioTestWithPtt>()
+
+        assertEquals(origin.online::class, result.online::class)
+        assertEquals(origin.offline::class, result.offline::class)
+        assertEquals(origin.onlineAsRec::class, result.onlineAsRec::class)
+        assertEquals(origin.offlineAsRec::class, result.offlineAsRec::class)
+
+        assertEquals(origin.online, result.online)
+        assertEquals(origin.offline, result.offline)
+        assertEquals(origin.onlineAsRec, result.onlineAsRec)
+        assertEquals(origin.offlineAsRec, result.offlineAsRec)
+
+        assertEquals(origin, result)
     }
 }

+ 12 - 4
settings.gradle.kts

@@ -19,6 +19,11 @@ pluginManagement {
 
 rootProject.name = "mirai"
 
+fun includeProject(projectPath: String, dir: String? = null) {
+    include(projectPath)
+    if (dir != null) project(projectPath).projectDir = file(dir)
+}
+
 include(":mirai-core-utils")
 include(":mirai-core-api")
 include(":mirai-core")
@@ -29,14 +34,17 @@ include(":binary-compatibility-validator-android")
 project(":binary-compatibility-validator-android").projectDir = file("binary-compatibility-validator/android")
 include(":ci-release-helper")
 
+includeProject(":mirai-logging-log4j2", "logging/mirai-logging-log4j2")
+includeProject(":mirai-logging-slf4j", "logging/mirai-logging-slf4j")
+includeProject(":mirai-logging-slf4j-simple", "logging/mirai-logging-slf4j-simple")
+includeProject(":mirai-logging-slf4j-logback", "logging/mirai-logging-slf4j-logback")
+
 
 fun includeConsoleProjects() {
     val disableOldFrontEnds = true
 
-    fun includeConsoleProject(projectPath: String, path: String? = null) {
-        include(projectPath)
-        if (path != null) project(projectPath).projectDir = file("mirai-console/$path")
-    }
+    fun includeConsoleProject(projectPath: String, dir: String? = null) =
+        includeProject(projectPath, "mirai-console/$dir")
 
     includeConsoleProject(":mirai-console-compiler-annotations", "tools/compiler-annotations")
     includeConsoleProject(":mirai-console", "backend/mirai-console")