Ver código fonte

ProtoBufWithNullableSupport

Him188 6 anos atrás
pai
commit
6b432b2aa6
22 arquivos alterados com 594 adições e 62 exclusões
  1. 12 9
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
  2. 4 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
  3. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/ForceOfflineEvent.kt
  4. 4 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/ProtoBuf.kt
  5. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt
  6. 483 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/ProtoBufWithNullableSupport.kt
  7. 5 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
  8. 12 12
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt
  9. 4 3
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgSvc.kt
  10. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/MessageMicro.kt
  11. 2 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
  12. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageDownPacket.kt
  13. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt
  14. 46 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
  15. 2 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt
  16. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/Register.kt
  17. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/DeviceInfo.kt
  18. 4 6
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt
  19. 1 2
      mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/packet/OutgoingPacket.kt
  20. 1 1
      mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/packet/PacketFactory.kt
  21. 1 1
      mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/utils/writeProto.kt
  22. 1 1
      mirai-debug/src/main/kotlin/test/ProtoTest.kt

+ 12 - 9
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt

@@ -1,6 +1,5 @@
 package net.mamoe.mirai.qqandroid
 
-import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.FriendNameRemark
 import net.mamoe.mirai.data.GroupInfo
@@ -8,16 +7,19 @@ import net.mamoe.mirai.data.PreviousNameList
 import net.mamoe.mirai.data.Profile
 import net.mamoe.mirai.message.data.ImageId
 import net.mamoe.mirai.message.data.MessageChain
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
 import net.mamoe.mirai.utils.*
 import kotlin.coroutines.CoroutineContext
 
 internal abstract class ContactImpl : Contact
 
-internal class QQImpl(bot: Bot, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), QQ {
-    override val bot: Bot by bot.unsafeWeakRef()
+internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), QQ {
+    override val bot: QQAndroidBot by bot.unsafeWeakRef()
 
     override suspend fun sendMessage(message: MessageChain) {
-        TODO("not implemented")
+        bot.network.run {
+            MessageSvc.PbSendMsg.ToFriend(bot.client, id, message).sendAndExpect<MessageSvc.PbSendMsg.Response>()
+        }
     }
 
     override suspend fun uploadImage(image: ExternalImage): ImageId {
@@ -38,11 +40,11 @@ internal class QQImpl(bot: Bot, override val coroutineContext: CoroutineContext,
 
 }
 
-internal class MemberImpl(bot: Bot, group: Group, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), Member {
+internal class MemberImpl(bot: QQAndroidBot, group: Group, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), Member {
     override val group: Group by group.unsafeWeakRef()
     override val permission: MemberPermission
         get() = TODO("not implemented")
-    override val bot: Bot by bot.unsafeWeakRef()
+    override val bot: QQAndroidBot by bot.unsafeWeakRef()
 
     override suspend fun mute(durationSeconds: Int): Boolean {
         TODO("not implemented")
@@ -76,7 +78,7 @@ internal class MemberImpl(bot: Bot, group: Group, override val coroutineContext:
 
 
 @UseExperimental(MiraiInternalAPI::class)
-internal class GroupImpl(bot: Bot, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), Group {
+internal class GroupImpl(bot: QQAndroidBot, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), Group {
     override val internalId: GroupInternalId = GroupId(id).toInternalId()
     override val owner: Member
         get() = TODO("not implemented")
@@ -86,7 +88,8 @@ internal class GroupImpl(bot: Bot, override val coroutineContext: CoroutineConte
         get() = TODO("not implemented")
     override val members: ContactList<Member> = ContactList(LockFreeLinkedList())
 
-    override fun getMember(id: Long): Member = members.delegate.filteringGetOrAdd({ it.id == id }, { MemberImpl(bot, this, coroutineContext, id) })
+    override fun getMember(id: Long): Member =
+        members.delegate.filteringGetOrAdd({ it.id == id }, { MemberImpl(bot as QQAndroidBot, this, coroutineContext, id) })
 
     override suspend fun updateGroupInfo(): GroupInfo {
         TODO("not implemented")
@@ -96,7 +99,7 @@ internal class GroupImpl(bot: Bot, override val coroutineContext: CoroutineConte
         TODO("not implemented")
     }
 
-    override val bot: Bot by bot.unsafeWeakRef()
+    override val bot: QQAndroidBot by bot.unsafeWeakRef()
 
     override suspend fun sendMessage(message: MessageChain) {
         TODO("not implemented")

+ 4 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt

@@ -33,7 +33,7 @@ internal abstract class QQAndroidBotBase constructor(
     override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
 
     override fun getQQ(id: Long): QQ {
-        return qqs.delegate.filteringGetOrAdd({ it.id == id }, { QQImpl(this, coroutineContext, id) })
+        return qqs.delegate.filteringGetOrAdd({ it.id == id }, { QQImpl(this as QQAndroidBot, coroutineContext, id) })
     }
 
     override fun createNetworkHandler(coroutineContext: CoroutineContext): QQAndroidBotNetworkHandler {
@@ -43,17 +43,17 @@ internal abstract class QQAndroidBotBase constructor(
     override val groups: ContactList<Group> = ContactList(LockFreeLinkedList())
 
     override suspend fun getGroup(id: GroupId): Group {
-        return groups.delegate.filteringGetOrAdd({ it.id == id.value }, { GroupImpl(this, coroutineContext, id.value) })
+        return groups.delegate.filteringGetOrAdd({ it.id == id.value }, { GroupImpl(this as QQAndroidBot, coroutineContext, id.value) })
     }
 
     override suspend fun getGroup(internalId: GroupInternalId): Group {
         internalId.toId().value.let { id ->
-            return groups.delegate.filteringGetOrAdd({ it.id == id }, { GroupImpl(this, coroutineContext, id) })
+            return groups.delegate.filteringGetOrAdd({ it.id == id }, { GroupImpl(this as QQAndroidBot, coroutineContext, id) })
         }
     }
 
     override suspend fun getGroup(id: Long): Group {
-        return groups.delegate.filteringGetOrAdd({ it.id == id }, { GroupImpl(this, coroutineContext, id) })
+        return groups.delegate.filteringGetOrAdd({ it.id == id }, { GroupImpl(this as QQAndroidBot, coroutineContext, id) })
     }
 
     override suspend fun Image.getLink(): ImageLink {

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/event/ForceOfflineEvent.kt

@@ -7,7 +7,7 @@ import net.mamoe.mirai.event.events.BotEvent
 /**
  * 被挤下线
  */
-class ForceOfflineEvent(
+data class ForceOfflineEvent(
     override val bot: Bot,
     val title: String,
     val tips: String

+ 4 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/ProtoBuf.kt

@@ -6,6 +6,7 @@ import kotlinx.io.core.readBytes
 import kotlinx.io.core.writeFully
 import kotlinx.serialization.DeserializationStrategy
 import kotlinx.serialization.SerializationStrategy
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 
 /**
  * 仅有标示作用
@@ -20,19 +21,19 @@ fun <T : ProtoBuf> BytePacketBuilder.writeProtoBuf(serializer: SerializationStra
  * dump
  */
 fun <T : ProtoBuf> T.toByteArray(serializer: SerializationStrategy<T>): ByteArray {
-    return kotlinx.serialization.protobuf.ProtoBuf.dump(serializer, this)
+    return ProtoBufWithNullableSupport.dump(serializer, this)
 }
 
 /**
  * load
  */
 fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrategy<T>): T {
-    return kotlinx.serialization.protobuf.ProtoBuf.load(deserializer, this)
+    return ProtoBufWithNullableSupport.load(deserializer, this)
 }
 
 /**
  * load
  */
 fun <T : ProtoBuf> Input.readRemainingAsProtoBuf(serializer: DeserializationStrategy<T>): T {
-    return kotlinx.serialization.protobuf.ProtoBuf.load(serializer, this.readBytes())
+    return ProtoBufWithNullableSupport.load(serializer, this.readBytes())
 }

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/Jce.kt

@@ -191,7 +191,7 @@ class Jce private constructor(private val charset: JceCharset, context: SerialMo
                         this.writeHead(STRUCT_END, 0)
                     }
                 } else if (value is ProtoBuf) {
-                    this.encodeTaggedByteArray(popTag(), kotlinx.serialization.protobuf.ProtoBuf.dump(value))
+                    this.encodeTaggedByteArray(popTag(), net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport.dump(value))
                 } else {
                     serializer.serialize(this, value)
                 }

+ 483 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/io/serialization/ProtoBufWithNullableSupport.kt

@@ -0,0 +1,483 @@
+/*
+ * Copyright 2017-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package net.mamoe.mirai.qqandroid.io.serialization
+
+import kotlinx.io.*
+import kotlinx.serialization.*
+import kotlinx.serialization.CompositeDecoder.Companion.READ_DONE
+import kotlinx.serialization.internal.*
+import kotlinx.serialization.modules.EmptyModule
+import kotlinx.serialization.modules.SerialModule
+import kotlinx.serialization.protobuf.ProtoNumberType
+import kotlinx.serialization.protobuf.ProtoType
+import kotlinx.serialization.protobuf.ProtobufDecodingException
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport.Varint.decodeSignedVarintInt
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport.Varint.decodeSignedVarintLong
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport.Varint.decodeVarint
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport.Varint.encodeVarint
+
+internal typealias ProtoDesc = Pair<Int, ProtoNumberType>
+
+internal fun extractParameters(desc: SerialDescriptor, index: Int, zeroBasedDefault: Boolean = false): ProtoDesc {
+    val idx = getSerialId(desc, index) ?: (if (zeroBasedDefault) index else index + 1)
+    val format = desc.findAnnotation<ProtoType>(index)?.type
+        ?: ProtoNumberType.DEFAULT
+    return idx to format
+}
+
+
+class ProtoBufWithNullableSupport(context: SerialModule = EmptyModule) : AbstractSerialFormat(context), BinaryFormat {
+
+    internal open inner class ProtobufWriter(val encoder: ProtobufEncoder) : TaggedEncoder<ProtoDesc>() {
+        public override val context
+            get() = [email protected]
+
+        override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeEncoder = when (desc.kind) {
+            StructureKind.LIST -> RepeatedWriter(encoder, currentTag)
+            StructureKind.CLASS, UnionKind.OBJECT, is PolymorphicKind -> ObjectWriter(currentTagOrNull, encoder)
+            StructureKind.MAP -> MapRepeatedWriter(currentTagOrNull, encoder)
+            else -> throw SerializationException("Primitives are not supported at top-level")
+        }
+
+        override fun encodeTaggedInt(tag: ProtoDesc, value: Int) = encoder.writeInt(value, tag.first, tag.second)
+        override fun encodeTaggedByte(tag: ProtoDesc, value: Byte) = encoder.writeInt(value.toInt(), tag.first, tag.second)
+        override fun encodeTaggedShort(tag: ProtoDesc, value: Short) = encoder.writeInt(value.toInt(), tag.first, tag.second)
+        override fun encodeTaggedLong(tag: ProtoDesc, value: Long) = encoder.writeLong(value, tag.first, tag.second)
+        override fun encodeTaggedFloat(tag: ProtoDesc, value: Float) = encoder.writeFloat(value, tag.first)
+        override fun encodeTaggedDouble(tag: ProtoDesc, value: Double) = encoder.writeDouble(value, tag.first)
+        override fun encodeTaggedBoolean(tag: ProtoDesc, value: Boolean) = encoder.writeInt(if (value) 1 else 0, tag.first, ProtoNumberType.DEFAULT)
+        override fun encodeTaggedChar(tag: ProtoDesc, value: Char) = encoder.writeInt(value.toInt(), tag.first, tag.second)
+        override fun encodeTaggedString(tag: ProtoDesc, value: String) = encoder.writeString(value, tag.first)
+        override fun encodeTaggedEnum(
+            tag: ProtoDesc,
+            enumDescription: SerialDescriptor,
+            ordinal: Int
+        ) = encoder.writeInt(
+            extractParameters(enumDescription, ordinal, zeroBasedDefault = true).first,
+            tag.first,
+            ProtoNumberType.DEFAULT
+        )
+
+        override fun SerialDescriptor.getTag(index: Int) = this.getProtoDesc(index)
+
+        // MIRAI MODIFY START:
+        override fun encodeTaggedNull(tag: ProtoDesc) {
+
+        }
+
+        override fun <T : Any> encodeNullableSerializableValue(serializer: SerializationStrategy<T>, value: T?) {
+            if (value == null) {
+                encodeTaggedNull(popTag())
+            } else encodeSerializableValue(serializer, value)
+        }
+        // MIRAI MODIFY END
+
+        @Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
+        override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) = when {
+            // encode maps as collection of map entries, not merged collection of key-values
+            serializer.descriptor is MapLikeDescriptor -> {
+                val serializer = (serializer as MapLikeSerializer<Any?, Any?, T, *>)
+                val mapEntrySerial = MapEntrySerializer(serializer.keySerializer, serializer.valueSerializer)
+                HashSetSerializer(mapEntrySerial).serialize(this, (value as Map<*, *>).entries)
+            }
+            serializer.descriptor == ByteArraySerializer.descriptor -> encoder.writeBytes(value as ByteArray, popTag().first)
+            else -> serializer.serialize(this, value)
+        }
+    }
+
+    internal open inner class ObjectWriter(
+        val parentTag: ProtoDesc?, private val parentEncoder: ProtobufEncoder,
+        private val stream: ByteArrayOutputStream = ByteArrayOutputStream()
+    ) : ProtobufWriter(ProtobufEncoder(stream)) {
+        override fun endEncode(desc: SerialDescriptor) {
+            if (parentTag != null) {
+                parentEncoder.writeBytes(stream.toByteArray(), parentTag.first)
+            } else {
+                parentEncoder.out.write(stream.toByteArray())
+            }
+        }
+    }
+
+    internal inner class MapRepeatedWriter(parentTag: ProtoDesc?, parentEncoder: ProtobufEncoder) : ObjectWriter(parentTag, parentEncoder) {
+        override fun SerialDescriptor.getTag(index: Int): ProtoDesc =
+            if (index % 2 == 0) 1 to (parentTag?.second ?: ProtoNumberType.DEFAULT)
+            else 2 to (parentTag?.second ?: ProtoNumberType.DEFAULT)
+    }
+
+    internal inner class RepeatedWriter(encoder: ProtobufEncoder, val curTag: ProtoDesc) : ProtobufWriter(encoder) {
+        override fun SerialDescriptor.getTag(index: Int) = curTag
+    }
+
+    internal class ProtobufEncoder(val out: ByteArrayOutputStream) {
+
+        fun writeBytes(bytes: ByteArray, tag: Int) {
+            val header = encode32((tag shl 3) or SIZE_DELIMITED)
+            val len = encode32(bytes.size)
+            out.write(header)
+            out.write(len)
+            out.write(bytes)
+        }
+
+        fun writeInt(value: Int, tag: Int, format: ProtoNumberType) {
+            val wireType = if (format == ProtoNumberType.FIXED) i32 else VARINT
+            val header = encode32((tag shl 3) or wireType)
+            val content = encode32(value, format)
+            out.write(header)
+            out.write(content)
+        }
+
+        fun writeLong(value: Long, tag: Int, format: ProtoNumberType) {
+            val wireType = if (format == ProtoNumberType.FIXED) i64 else VARINT
+            val header = encode32((tag shl 3) or wireType)
+            val content = encode64(value, format)
+            out.write(header)
+            out.write(content)
+        }
+
+        fun writeString(value: String, tag: Int) {
+            val bytes = value.toUtf8Bytes()
+            writeBytes(bytes, tag)
+        }
+
+        fun writeDouble(value: Double, tag: Int) {
+            val header = encode32((tag shl 3) or i64)
+            val content = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array()
+            out.write(header)
+            out.write(content)
+        }
+
+        fun writeFloat(value: Float, tag: Int) {
+            val header = encode32((tag shl 3) or i32)
+            val content = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array()
+            out.write(header)
+            out.write(content)
+        }
+
+        private fun encode32(number: Int, format: ProtoNumberType = ProtoNumberType.DEFAULT): ByteArray =
+            when (format) {
+                ProtoNumberType.FIXED -> ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(number).array()
+                ProtoNumberType.DEFAULT -> encodeVarint(number.toLong())
+                ProtoNumberType.SIGNED -> encodeVarint(((number shl 1) xor (number shr 31)))
+            }
+
+
+        private fun encode64(number: Long, format: ProtoNumberType = ProtoNumberType.DEFAULT): ByteArray =
+            when (format) {
+                ProtoNumberType.FIXED -> ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(number).array()
+                ProtoNumberType.DEFAULT -> encodeVarint(number)
+                ProtoNumberType.SIGNED -> encodeVarint((number shl 1) xor (number shr 63))
+            }
+    }
+
+    private open inner class ProtobufReader(val decoder: ProtobufDecoder) : TaggedDecoder<ProtoDesc>() {
+        override val context: SerialModule
+            get() = [email protected]
+
+        private val indexByTag: MutableMap<Int, Int> = mutableMapOf()
+        private fun findIndexByTag(desc: SerialDescriptor, serialId: Int, zeroBasedDefault: Boolean = false): Int =
+            (0 until desc.elementsCount).firstOrNull {
+                extractParameters(
+                    desc,
+                    it,
+                    zeroBasedDefault
+                ).first == serialId
+            } ?: -1
+
+        override fun beginStructure(desc: SerialDescriptor, vararg typeParams: KSerializer<*>): CompositeDecoder = when (desc.kind) {
+            StructureKind.LIST -> RepeatedReader(decoder, currentTag)
+            StructureKind.CLASS, UnionKind.OBJECT, is PolymorphicKind ->
+                ProtobufReader(makeDelimited(decoder, currentTagOrNull))
+            StructureKind.MAP -> MapEntryReader(makeDelimited(decoder, currentTagOrNull), currentTagOrNull)
+            else -> throw SerializationException("Primitives are not supported at top-level")
+        }
+
+        override fun decodeTaggedBoolean(tag: ProtoDesc): Boolean = when (val i = decoder.nextInt(ProtoNumberType.DEFAULT)) {
+            0 -> false
+            1 -> true
+            else -> throw ProtobufDecodingException("Expected boolean value (0 or 1), found $i")
+        }
+
+        override fun decodeTaggedByte(tag: ProtoDesc): Byte = decoder.nextInt(tag.second).toByte()
+        override fun decodeTaggedShort(tag: ProtoDesc): Short = decoder.nextInt(tag.second).toShort()
+        override fun decodeTaggedInt(tag: ProtoDesc): Int = decoder.nextInt(tag.second)
+        override fun decodeTaggedLong(tag: ProtoDesc): Long = decoder.nextLong(tag.second)
+        override fun decodeTaggedFloat(tag: ProtoDesc): Float = decoder.nextFloat()
+        override fun decodeTaggedDouble(tag: ProtoDesc): Double = decoder.nextDouble()
+        override fun decodeTaggedChar(tag: ProtoDesc): Char = decoder.nextInt(tag.second).toChar()
+        override fun decodeTaggedString(tag: ProtoDesc): String = decoder.nextString()
+        override fun decodeTaggedEnum(tag: ProtoDesc, enumDescription: SerialDescriptor): Int =
+            findIndexByTag(enumDescription, decoder.nextInt(ProtoNumberType.DEFAULT), zeroBasedDefault = true)
+
+        @Suppress("UNCHECKED_CAST")
+        override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when {
+            // encode maps as collection of map entries, not merged collection of key-values
+            deserializer.descriptor is MapLikeDescriptor -> {
+                val serializer = (deserializer as MapLikeSerializer<Any?, Any?, T, *>)
+                val mapEntrySerial = MapEntrySerializer(serializer.keySerializer, serializer.valueSerializer)
+                val setOfEntries = HashSetSerializer(mapEntrySerial).deserialize(this)
+                setOfEntries.associateBy({ it.key }, { it.value }) as T
+            }
+            deserializer.descriptor == ByteArraySerializer.descriptor -> decoder.nextObject() as T
+            else -> deserializer.deserialize(this)
+        }
+
+        override fun SerialDescriptor.getTag(index: Int) = this.getProtoDesc(index)
+
+        override fun decodeElementIndex(desc: SerialDescriptor): Int {
+            while (true) {
+                if (decoder.curId == -1) // EOF
+                    return READ_DONE
+                val ind = indexByTag.getOrPut(decoder.curId) { findIndexByTag(desc, decoder.curId) }
+                if (ind == -1) // not found
+                    decoder.skipElement()
+                else return ind
+            }
+        }
+    }
+
+    private inner class RepeatedReader(decoder: ProtobufDecoder, val targetTag: ProtoDesc) : ProtobufReader(decoder) {
+        private var ind = -1
+
+        override fun decodeElementIndex(desc: SerialDescriptor) = if (decoder.curId == targetTag.first) ++ind else READ_DONE
+        override fun SerialDescriptor.getTag(index: Int): ProtoDesc = targetTag
+    }
+
+    private inner class MapEntryReader(decoder: ProtobufDecoder, val parentTag: ProtoDesc?) : ProtobufReader(decoder) {
+        override fun SerialDescriptor.getTag(index: Int): ProtoDesc =
+            if (index % 2 == 0) 1 to (parentTag?.second ?: ProtoNumberType.DEFAULT)
+            else 2 to (parentTag?.second ?: ProtoNumberType.DEFAULT)
+    }
+
+    internal class ProtobufDecoder(val inp: ByteArrayInputStream) {
+        val curId
+            get() = curTag.first
+        private var curTag: Pair<Int, Int> = -1 to -1
+
+        init {
+            readTag()
+        }
+
+        private fun readTag(): Pair<Int, Int> {
+            val header = decode32(eofAllowed = true)
+            curTag = if (header == -1) {
+                -1 to -1
+            } else {
+                val wireType = header and 0b111
+                val fieldId = header ushr 3
+                fieldId to wireType
+            }
+            return curTag
+        }
+
+        fun skipElement() {
+            when (curTag.second) {
+                VARINT -> nextInt(ProtoNumberType.DEFAULT)
+                i64 -> nextLong(ProtoNumberType.FIXED)
+                SIZE_DELIMITED -> nextObject()
+                i32 -> nextInt(ProtoNumberType.FIXED)
+                else -> throw ProtobufDecodingException("Unsupported start group or end group wire type")
+            }
+        }
+
+        @Suppress("NOTHING_TO_INLINE")
+        private inline fun assertWireType(expected: Int) {
+            if (curTag.second != expected) throw ProtobufDecodingException("Expected wire type $expected, but found ${curTag.second}")
+        }
+
+        fun nextObject(): ByteArray {
+            assertWireType(SIZE_DELIMITED)
+            val len = decode32()
+            check(len >= 0)
+            val ans = inp.readExactNBytes(len)
+            readTag()
+            return ans
+        }
+
+        fun nextInt(format: ProtoNumberType): Int {
+            val wireType = if (format == ProtoNumberType.FIXED) i32 else VARINT
+            assertWireType(wireType)
+            val ans = decode32(format)
+            readTag()
+            return ans
+        }
+
+        fun nextLong(format: ProtoNumberType): Long {
+            val wireType = if (format == ProtoNumberType.FIXED) i64 else VARINT
+            assertWireType(wireType)
+            val ans = decode64(format)
+            readTag()
+            return ans
+        }
+
+        fun nextFloat(): Float {
+            assertWireType(i32)
+            val ans = inp.readToByteBuffer(4).order(ByteOrder.LITTLE_ENDIAN).getFloat()
+            readTag()
+            return ans
+        }
+
+        fun nextDouble(): Double {
+            assertWireType(i64)
+            val ans = inp.readToByteBuffer(8).order(ByteOrder.LITTLE_ENDIAN).getDouble()
+            readTag()
+            return ans
+        }
+
+        fun nextString(): String {
+            val bytes = this.nextObject()
+            return stringFromUtf8Bytes(bytes)
+        }
+
+        private fun decode32(format: ProtoNumberType = ProtoNumberType.DEFAULT, eofAllowed: Boolean = false): Int = when (format) {
+            ProtoNumberType.DEFAULT -> decodeVarint(inp, 64, eofAllowed).toInt()
+            ProtoNumberType.SIGNED -> decodeSignedVarintInt(inp)
+            ProtoNumberType.FIXED -> inp.readToByteBuffer(4).order(ByteOrder.LITTLE_ENDIAN).getInt()
+        }
+
+        private fun decode64(format: ProtoNumberType = ProtoNumberType.DEFAULT): Long = when (format) {
+            ProtoNumberType.DEFAULT -> decodeVarint(inp, 64)
+            ProtoNumberType.SIGNED -> decodeSignedVarintLong(inp)
+            ProtoNumberType.FIXED -> inp.readToByteBuffer(8).order(ByteOrder.LITTLE_ENDIAN).getLong()
+        }
+    }
+
+    /**
+     *  Source for all varint operations:
+     *  https://github.com/addthis/stream-lib/blob/master/src/main/java/com/clearspring/analytics/util/Varint.java
+     */
+    internal object Varint {
+        internal fun encodeVarint(inp: Int): ByteArray {
+            var value = inp
+            val byteArrayList = ByteArray(10)
+            var i = 0
+            while (value and 0xFFFFFF80.toInt() != 0) {
+                byteArrayList[i++] = ((value and 0x7F) or 0x80).toByte()
+                value = value ushr 7
+            }
+            byteArrayList[i] = (value and 0x7F).toByte()
+            val out = ByteArray(i + 1)
+            while (i >= 0) {
+                out[i] = byteArrayList[i]
+                i--
+            }
+            return out
+        }
+
+        internal fun encodeVarint(inp: Long): ByteArray {
+            var value = inp
+            val byteArrayList = ByteArray(10)
+            var i = 0
+            while (value and 0x7FL.inv() != 0L) {
+                byteArrayList[i++] = ((value and 0x7F) or 0x80).toByte()
+                value = value ushr 7
+            }
+            byteArrayList[i] = (value and 0x7F).toByte()
+            val out = ByteArray(i + 1)
+            while (i >= 0) {
+                out[i] = byteArrayList[i]
+                i--
+            }
+            return out
+        }
+
+        internal fun decodeVarint(inp: InputStream, bitLimit: Int = 32, eofOnStartAllowed: Boolean = false): Long {
+            var result = 0L
+            var shift = 0
+            var b: Int
+            do {
+                if (shift >= bitLimit) {
+                    // Out of range
+                    throw ProtobufDecodingException("Varint too long: exceeded $bitLimit bits")
+                }
+                // Get 7 bits from next byte
+                b = inp.read()
+                if (b == -1) {
+                    if (eofOnStartAllowed && shift == 0) return -1
+                    else throw IOException("Unexpected EOF")
+                }
+                result = result or (b.toLong() and 0x7FL shl shift)
+                shift += 7
+            } while (b and 0x80 != 0)
+            return result
+        }
+
+        internal fun decodeSignedVarintInt(inp: InputStream): Int {
+            val raw = decodeVarint(inp, 32).toInt()
+            val temp = raw shl 31 shr 31 xor raw shr 1
+            // This extra step lets us deal with the largest signed values by treating
+            // negative results from read unsigned methods as like unsigned values.
+            // Must re-flip the top bit if the original read value had it set.
+            return temp xor (raw and (1 shl 31))
+        }
+
+        internal fun decodeSignedVarintLong(inp: InputStream): Long {
+            val raw = decodeVarint(inp, 64)
+            val temp = raw shl 63 shr 63 xor raw shr 1
+            // This extra step lets us deal with the largest signed values by treating
+            // negative results from read unsigned methods as like unsigned values
+            // Must re-flip the top bit if the original read value had it set.
+            return temp xor (raw and (1L shl 63))
+
+        }
+    }
+
+    companion object : BinaryFormat {
+        public override val context: SerialModule get() = plain.context
+
+        // todo: make more memory-efficient
+        private fun makeDelimited(decoder: ProtobufDecoder, parentTag: ProtoDesc?): ProtobufDecoder {
+            if (parentTag == null) return decoder
+            val bytes = decoder.nextObject()
+            return ProtobufDecoder(ByteArrayInputStream(bytes))
+        }
+
+        private fun SerialDescriptor.getProtoDesc(index: Int): ProtoDesc {
+            return extractParameters(this, index)
+        }
+
+        internal const val VARINT = 0
+        internal const val i64 = 1
+        internal const val SIZE_DELIMITED = 2
+        internal const val i32 = 5
+
+        val plain = ProtoBufWithNullableSupport()
+
+        override fun <T> dump(serializer: SerializationStrategy<T>, obj: T): ByteArray = plain.dump(serializer, obj)
+        override fun <T> load(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T = plain.load(deserializer, bytes)
+        override fun install(module: SerialModule) = throw IllegalStateException("You should not install anything to global instance")
+    }
+
+    override fun <T> dump(serializer: SerializationStrategy<T>, obj: T): ByteArray {
+        val encoder = ByteArrayOutputStream()
+        val dumper = ProtobufWriter(ProtobufEncoder(encoder))
+        dumper.encode(serializer, obj)
+        return encoder.toByteArray()
+    }
+
+    override fun <T> load(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T {
+        val stream = ByteArrayInputStream(bytes)
+        val reader = ProtobufReader(ProtobufDecoder(stream))
+        return reader.decode(deserializer)
+    }
+
+}
+
+internal fun InputStream.readExactNBytes(bytes: Int): ByteArray {
+    val array = ByteArray(bytes)
+    var read = 0
+    while (read < bytes) {
+        val i = this.read(array, read, bytes - read)
+        if (i == -1) throw IOException("Unexpected EOF")
+        read += i
+    }
+    return array
+}
+
+internal fun InputStream.readToByteBuffer(bytes: Int): ByteBuffer {
+    val arr = readExactNBytes(bytes)
+    val buf = ByteBuffer.allocate(bytes)
+    buf.put(arr).flip()
+    return buf
+}

+ 5 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt

@@ -318,10 +318,14 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
     suspend fun <E : Packet> OutgoingPacket.sendAndExpect(): E {
         val handler = PacketListener(commandName = commandName, sequenceId = sequenceId)
         packetListeners.addLast(handler)
+        bot.logger.info("Send: ${this.commandName}")
         channel.send(delegate)
-        return withTimeout(3000) {
+        return withTimeoutOrNull(3000) {
             @Suppress("UNCHECKED_CAST")
             handler.await() as E
+        } ?: net.mamoe.mirai.qqandroid.utils.inline {
+            packetListeners.remove(handler)
+            error("timeout when sending ${this.commandName}")
         }
     }
 

+ 12 - 12
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/Msg.kt

@@ -609,7 +609,7 @@ class ImMsgBody : ProtoBuf {
         @SerialId(8) val reserved: Int = 0,
         @SerialId(9) val subcmd: Int = 0,
         @SerialId(10) val microCloud: Int = 0,
-        @SerialId(11) val bytesFileUrls: List<ByteArray>? = null,
+        @SerialId(11) val bytesFileUrls: List<ByteArray>? = listOf(),
         @SerialId(12) val downloadFlag: Int = 0,
         @SerialId(50) val dangerEvel: Int = 0,
         @SerialId(51) val lifeTime: Int = 0,
@@ -705,7 +705,7 @@ class ImMsgBody : ProtoBuf {
         @SerialId(20) val downPara: ByteArray = EMPTY_BYTE_ARRAY,
         @SerialId(29) val format: Int = 0,
         @SerialId(30) val pbReserve: ByteArray = EMPTY_BYTE_ARRAY,
-        @SerialId(31) val bytesPttUrls: List<ByteArray>? = null,
+        @SerialId(31) val bytesPttUrls: List<ByteArray>? = listOf(),
         @SerialId(32) val downloadFlag: Int = 0
     ) : ProtoBuf
 
@@ -811,12 +811,12 @@ class ImMsgBody : ProtoBuf {
 
     @Serializable
     class RichText(
-        @SerialId(1) val attr: Attr? = null,
+        @SerialId(1) val attr: Attr? = Attr(),
         @SerialId(2) val elems: MutableList<Elem> = mutableListOf(),
-        @SerialId(3) val notOnlineFile: NotOnlineFile? = null,
-        @SerialId(4) val ptt: Ptt? = null,
-        @SerialId(5) val tmpPtt: TmpPtt? = null,
-        @SerialId(6) val trans211TmpMsg: Trans211TmpMsg? = null
+        @SerialId(3) val notOnlineFile: NotOnlineFile? = NotOnlineFile(),
+        @SerialId(4) val ptt: Ptt? = Ptt(),
+        @SerialId(5) val tmpPtt: TmpPtt? = TmpPtt(),
+        @SerialId(6) val trans211TmpMsg: Trans211TmpMsg? = Trans211TmpMsg()
     ) : ProtoBuf
 
     @Serializable
@@ -1045,9 +1045,9 @@ class ImMsgHead : ProtoBuf {
 
     @Serializable
     class InstCtrl(
-        @SerialId(1) val msgSendToInst: List<InstInfo>? = null,
-        @SerialId(2) val msgExcludeInst: List<InstInfo>? = null,
-        @SerialId(3) val msgFromInst: InstInfo? = null
+        @SerialId(1) val msgSendToInst: List<InstInfo>? = listOf(),
+        @SerialId(2) val msgExcludeInst: List<InstInfo>? = listOf(),
+        @SerialId(3) val msgFromInst: InstInfo? = InstInfo()
     ) : ProtoBuf
 
     @Serializable
@@ -1107,7 +1107,7 @@ class ImReceipt : ProtoBuf {
     ) : ProtoBuf
 
     @Serializable
-    class ReceiptInfo(
+    data class ReceiptInfo(
         @SerialId(1) val readTime: Long = 0L
     ) : ProtoBuf
 
@@ -1118,7 +1118,7 @@ class ImReceipt : ProtoBuf {
     ) : ProtoBuf
 
     @Serializable
-    class ReceiptResp(
+    data class ReceiptResp(
         @SerialId(1) val command: Int /* enum */ = 1,
         @SerialId(2) val receiptInfo: ReceiptInfo? = null
     ) : ProtoBuf

+ 4 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/MsgSvc.kt

@@ -2,6 +2,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.data.proto
 
 import kotlinx.serialization.SerialId
 import kotlinx.serialization.Serializable
+import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.qqandroid.io.ProtoBuf
 import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 
@@ -102,7 +103,7 @@ class MsgSvc : ProtoBuf {
     ) : ProtoBuf
 
     @Serializable
-    class MsgSendInfo(
+    data class MsgSendInfo(
         @SerialId(1) val receiver: Int = 0
     ) : ProtoBuf
 
@@ -440,7 +441,7 @@ class MsgSvc : ProtoBuf {
     ) : ProtoBuf
 
     @Serializable
-    class PbSendMsgResp(
+    data class PbSendMsgResp(
         @SerialId(1) val result: Int = 0,
         @SerialId(2) val errmsg: String = "",
         @SerialId(3) val sendTime: Int = 0,
@@ -450,7 +451,7 @@ class MsgSvc : ProtoBuf {
         @SerialId(7) val transSvrInfo: TransSvrInfo? = null,
         @SerialId(8) val receiptResp: ImReceipt.ReceiptResp? = null,
         @SerialId(9) val textAnalysisResult: Int = 0
-    ) : ProtoBuf
+    ) : ProtoBuf, Packet
 
     @Serializable
     class PbBindUinUnReadMsgNumResp(

+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/MessageMicro.kt

@@ -1,9 +1,9 @@
 package net.mamoe.mirai.qqandroid.network.protocol.packet
 
 import kotlinx.serialization.SerializationStrategy
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 
 interface MessageMicro
 
 
-fun <T : MessageMicro> T.toByteArray(serializer: SerializationStrategy<T>): ByteArray = ProtoBuf.dump(serializer, this)
+fun <T : MessageMicro> T.toByteArray(serializer: SerializationStrategy<T>): ByteArray = ProtoBufWithNullableSupport.dump(serializer, this)

+ 2 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt

@@ -62,7 +62,8 @@ internal object KnownPacketFactories : List<PacketFactory<*>> by mutableListOf(
     OnlinePush.PbPushGroupMsg,
     MessageSvc.PushNotify,
     MessageSvc.PbGetMsg,
-    MessageSvc.PushForceOffline
+    MessageSvc.PushForceOffline,
+    MessageSvc.PbSendMsg
 ) {
     // SvcReqMSFLoginNotify 自己的其他设备上限
     // MessageSvc.PushReaded 电脑阅读了别人的消息, 告知手机

+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageDownPacket.kt

@@ -2,7 +2,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
 
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.writeFully
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
@@ -19,7 +19,7 @@ internal object ImageDownPacket : PacketFactory<ImageDownPacket.ImageDownPacketR
         // TODO: 2020/1/24 测试: bodyType, subAppId
         return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
             writeSsoPacket(client, subAppId = 0, commandName = commandName, sequenceId = it) {
-                val data = ProtoBuf.dump(
+                val data = ProtoBufWithNullableSupport.dump(
                     Cmd0x352Packet.serializer(),
                     Cmd0x352Packet.createByImageRequest(req)
                 )

+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/image/ImageUpPacket.kt

@@ -2,7 +2,7 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image
 
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.writeFully
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
@@ -19,7 +19,7 @@ internal object ImageUpPacket : PacketFactory<ImageUpPacket.ImageUpPacketRespons
         // TODO: 2020/1/24 测试: bodyType, subAppId
         return buildLoginOutgoingPacket(client, key = client.wLoginSigInfo.d2Key, bodyType = 1) {
             writeSsoPacket(client, subAppId = 0, commandName = ImageDownPacket.commandName, sequenceId = it) {
-                val data = ProtoBuf.dump(
+                val data = ProtoBufWithNullableSupport.dump(
                     Cmd0x352Packet.serializer(),
                     Cmd0x352Packet.createByImageRequest(req)
                 )

+ 46 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt

@@ -3,7 +3,9 @@ package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
 import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.discardExact
 import net.mamoe.mirai.data.MultiPacket
+import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.message.FriendMessage
+import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.event.ForceOfflineEvent
 import net.mamoe.mirai.qqandroid.io.readRemainingAsProtoBuf
@@ -12,18 +14,22 @@ import net.mamoe.mirai.qqandroid.io.writeProtoBuf
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushForceOffline
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushNotify
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgComm
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.qqandroid.utils.toMessageChain
+import net.mamoe.mirai.qqandroid.utils.toRichText
 import net.mamoe.mirai.utils.cryptor.contentToString
 import net.mamoe.mirai.utils.io.hexToBytes
 import net.mamoe.mirai.utils.io.toReadPacket
+import kotlin.random.Random
 
 class MessageSvc {
     /**
-     * 告知要刷新消息
+     * 告知要刷新好友消息
      */
     internal object PushNotify : PacketFactory<RequestPushNotify>("MessageSvc.PushNotify") {
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): RequestPushNotify {
@@ -41,7 +47,7 @@ class MessageSvc {
 
 
     /**
-     * 进行刷新消息
+     * 获取好友消息和消息记录
      */
     internal object PbGetMsg : PacketFactory<MultiPacket<FriendMessage>>("MessageSvc.PbGetMsg") {
         val EXTRA_DATA =
@@ -65,9 +71,10 @@ class MessageSvc {
                     onlineSyncFlag = 1,
                     //  serverBuf = from.serverBuf ?: EMPTY_BYTE_ARRAY,
                     syncCookie = client.c2cMessageSync.syncCookie,
-                    syncFlag = client.c2cMessageSync.syncFlag,
-                    msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf,
-                    pubaccountCookie = client.c2cMessageSync.pubAccountCookie
+                    syncFlag = 1
+                    // syncFlag = client.c2cMessageSync.syncFlag,
+                    //msgCtrlBuf = client.c2cMessageSync.msgCtrlBuf,
+                    //pubaccountCookie = client.c2cMessageSync.pubAccountCookie
                 )
             )
         }
@@ -112,5 +119,39 @@ class MessageSvc {
             return ForceOfflineEvent(bot, title = struct.title ?: "", tips = struct.tips ?: "")
         }
     }
+
+    internal object PbSendMsg : PacketFactory<MsgSvc.PbSendMsgResp>("MessageSvc.PbSendMsg") {
+        object Response : Packet
+
+        /**
+         * 发送好友消息
+         */
+        fun ToFriend(
+            client: QQAndroidClient,
+            toUin: Long,
+            message: MessageChain
+        ): OutgoingPacket = buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                MsgSvc.PbSendMsgReq.serializer(), MsgSvc.PbSendMsgReq(
+                    routingHead = MsgSvc.RoutingHead(c2c = MsgSvc.C2C(toUin = toUin)),
+                    contentHead = MsgComm.ContentHead(pkgNum = 1),
+                    msgBody = ImMsgBody.MsgBody(
+                        richText = message.toRichText().apply {
+                            elems.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
+                        }
+                    ),
+                    msgSeq = 15741,
+                    msgRand = Random.nextInt(),
+                    syncCookie = client.c2cMessageSync.syncCookie,
+                    msgVia = 1
+                )
+            )
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): MsgSvc.PbSendMsgResp {
+            discardExact(4)
+            return readRemainingAsProtoBuf(MsgSvc.PbSendMsgResp.serializer())
+        }
+    }
 }
 

Diferenças do arquivo suprimidas por serem muito extensas
+ 2 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt


+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/login/Register.kt

@@ -1,7 +1,7 @@
 package net.mamoe.mirai.qqandroid.network.protocol.packet.login
 
 import kotlinx.io.core.ByteReadPacket
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
@@ -103,7 +103,7 @@ class StatSvc {
                                         var44.strVendorName = ROMUtil.getRomName();
                                         var44.strVendorOSName = ROMUtil.getRomVersion(20);
                                         */
-                                        bytes_0x769_reqbody = ProtoBuf.dump(
+                                        bytes_0x769_reqbody = ProtoBufWithNullableSupport.dump(
                                             Oidb0x769.RequestBody.serializer(), Oidb0x769.RequestBody(
                                                 rpt_config_list = listOf(
                                                     Oidb0x769.ConfigSeq(

+ 2 - 2
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/DeviceInfo.kt

@@ -2,7 +2,7 @@ package net.mamoe.mirai.qqandroid.utils
 
 import kotlinx.serialization.SerialId
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBufWithNullableSupport
 import net.mamoe.mirai.utils.cryptor.contentToString
 import net.mamoe.mirai.utils.getValue
 import net.mamoe.mirai.utils.unsafeWeakRef
@@ -62,7 +62,7 @@ abstract class DeviceInfo(
             @SerialId(9) val innerVersion: ByteArray
         )
 
-        return ProtoBuf.dump(
+        return ProtoBufWithNullableSupport.dump(
             DevInfo.serializer(), DevInfo(
                 bootloader,
                 procVersion,

+ 4 - 6
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/MessageQQA.kt

@@ -3,16 +3,15 @@ package net.mamoe.mirai.qqandroid.utils
 import net.mamoe.mirai.data.ImageLink
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
-import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
 
 
-internal fun MessageChain.constructPbSendMsgReq(): MsgSvc.PbSendMsgReq {
-    val request = MsgSvc.PbSendMsgReq()
+internal fun MessageChain.toRichText(): ImMsgBody.RichText {
+    val richText = ImMsgBody.RichText()
 
     this.forEach {
         when (it) {
             is PlainText -> {
-                request.msgBody.richText.elems.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
+                richText.elems.add(ImMsgBody.Elem(text = ImMsgBody.Text(str = it.stringValue)))
             }
             is At -> {
 
@@ -20,8 +19,7 @@ internal fun MessageChain.constructPbSendMsgReq(): MsgSvc.PbSendMsgReq {
         }
     }
 
-
-    return request
+    return richText
 }
 
 

+ 1 - 2
mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/packet/OutgoingPacket.kt

@@ -4,10 +4,9 @@ package net.mamoe.mirai.timpc.network.packet
 
 import kotlinx.io.core.*
 import kotlinx.serialization.SerializationStrategy
-import kotlinx.serialization.protobuf.ProtoBuf
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.network.BotNetworkHandler
-
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBuf
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.cryptor.encryptAndWrite
 import net.mamoe.mirai.utils.io.hexToBytes

+ 1 - 1
mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/network/packet/PacketFactory.kt

@@ -9,9 +9,9 @@ import kotlinx.io.core.discardExact
 import kotlinx.io.core.readBytes
 import kotlinx.io.pool.useInstance
 import kotlinx.serialization.DeserializationStrategy
-import kotlinx.serialization.protobuf.ProtoBuf
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBuf
 import net.mamoe.mirai.utils.cryptor.Decrypter
 import net.mamoe.mirai.utils.cryptor.DecrypterType
 import net.mamoe.mirai.utils.cryptor.readProtoMap

+ 1 - 1
mirai-core-timpc/src/commonMain/kotlin/net.mamoe.mirai.timpc/utils/writeProto.kt

@@ -3,6 +3,6 @@ package net.mamoe.mirai.timpc.utils
 import kotlinx.io.core.BytePacketBuilder
 import kotlinx.io.core.writeFully
 import kotlinx.serialization.SerializationStrategy
-import kotlinx.serialization.protobuf.ProtoBuf
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBuf
 
 fun <T> BytePacketBuilder.writeProto(serializer: SerializationStrategy<T>, obj: T) = writeFully(ProtoBuf.dump(serializer, obj))

+ 1 - 1
mirai-debug/src/main/kotlin/test/ProtoTest.kt

@@ -5,10 +5,10 @@ package test
 import kotlinx.serialization.ImplicitReflectionSerializer
 import kotlinx.serialization.SerialId
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.protobuf.ProtoBuf
 import kotlinx.serialization.protobuf.ProtoNumberType
 import kotlinx.serialization.protobuf.ProtoType
 import kotlinx.serialization.serializer
+import net.mamoe.mirai.qqandroid.io.serialization.ProtoBuf
 import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.cryptor.readProtoMap
 import net.mamoe.mirai.utils.io.hexToBytes

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff