Bläddra i källkod

Merge remote-tracking branch 'origin/dev' into dev

Him188 5 år sedan
förälder
incheckning
522bcba33e

+ 19 - 3
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.common.kt

@@ -11,7 +11,6 @@
 
 package net.mamoe.mirai.qqandroid
 
-import io.ktor.client.HttpClient
 import io.ktor.client.request.*
 import io.ktor.client.request.forms.MultiPartFormDataContent
 import io.ktor.client.request.forms.formData
@@ -45,6 +44,7 @@ import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.ImMsgBody
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.LongMsg
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.*
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.voice.PttStore
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.qqandroid.utils.MiraiPlatformUtils
 import net.mamoe.mirai.qqandroid.utils.encodeToString
@@ -56,6 +56,7 @@ import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmField
 import kotlin.jvm.JvmSynthetic
 import kotlin.math.absoluteValue
+import kotlin.math.log
 import kotlin.random.Random
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FriendInfo as JceFriendInfo
 
@@ -560,7 +561,7 @@ internal abstract class QQAndroidBotBase constructor(
     @MiraiExperimentalAPI
     override suspend fun _lowLevelGetAnnouncement(groupId: Long, fid: String): GroupAnnouncement {
         val data = network.async {
-            HttpClient().post<String> {
+            MiraiPlatformUtils.Http.post<String> {
                 url("https://web.qun.qq.com/cgi-bin/announce/get_feed")
                 body = MultiPartFormDataContent(formData {
                     append("qid", groupId)
@@ -587,7 +588,7 @@ internal abstract class QQAndroidBotBase constructor(
     @MiraiExperimentalAPI
     override suspend fun _lowLevelGetGroupActiveData(groupId: Long, page: Int): GroupActiveData {
         val data = network.async {
-            HttpClient().get<String> {
+           MiraiPlatformUtils.Http.get<String> {
                 url("https://qqweb.qq.com/c/activedata/get_mygroup_data")
                 parameter("bkn", bkn)
                 parameter("gc", groupId)
@@ -792,6 +793,21 @@ internal abstract class QQAndroidBotBase constructor(
         }
     }
 
+    @ExperimentalStdlibApi
+    @MiraiExperimentalAPI
+    @LowLevelAPI
+    override suspend fun _lowLevelQueryGroupVoiceDownloadUrl(
+        md5: ByteArray,
+        groupId: Long,
+        dstUin: Long
+    ): String {
+          network.run {
+            val response: PttStore.GroupPttDown.Response.DownLoadInfo =
+                PttStore.GroupPttDown(client, groupId, dstUin,md5).sendAndExpect()
+            return "http://${response.strDomain}${response.downPara.encodeToString()}"
+        }
+    }
+
     @Suppress("DEPRECATION", "OverridingDeprecatedMember")
     override suspend fun queryImageUrl(image: Image): String = when (image) {
         is ConstOriginUrlAware -> image.originUrl

+ 26 - 9
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/conversions.kt

@@ -32,7 +32,9 @@ import kotlin.contracts.contract
 private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
 private val UNSUPPORTED_POKE_MESSAGE_PLAIN = PlainText("[戳一戳]请使用最新版手机QQ体验新功能。")
 private val UNSUPPORTED_FLASH_MESSAGE_PLAIN = PlainText("[闪照]请使用新版手机QQ查看闪照。")
+private val UNSUPPORTED_VOICE_MESSAGE_PLAIN = PlainText("收到语音消息,你需要升级到最新版QQ才能接收,升级地址https://im.qq.com")
 
+@OptIn(ExperimentalStdlibApi::class)
 @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
 internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: Boolean): MutableList<ImMsgBody.Elem> {
     val elements = ArrayList<ImMsgBody.Elem>(this.size)
@@ -156,7 +158,20 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: B
             is VipFace -> {
                 transformOneMessage(PlainText(it.contentToString()))
             }
-            is PttMessage,
+            is PttMessage -> {
+                elements.add(
+                    ImMsgBody.Elem(
+                        extraInfo = ImMsgBody.ExtraInfo(flags = 16, groupMask = 1)
+                    )
+                )
+                elements.add(
+                    ImMsgBody.Elem(
+                        elemFlags2 = ImMsgBody.ElemFlags2(
+                            vipStatus = 1
+                        )
+                    )
+                )
+            }
             is ForwardMessage,
             is MessageSource, // mirai metadata only
             is RichMessage // already transformed above
@@ -218,10 +233,11 @@ internal fun MsgComm.Msg.toMessageChain(
     val ptt = this.msgBody.richText.ptt
 
     val pptMsg = ptt?.run {
-        when(fileType) {
-            4 -> Voice(String(fileName), fileMd5, String(downPara))
-            else -> null
-        }
+//        when (fileType) {
+//            4 -> Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
+//            else -> null
+//        }
+        Voice(String(fileName), fileMd5, fileSize.toLong(),String(downPara))
     }
 
     return buildMessageChain(elements.size + 1 + if (pptMsg == null) 0 else 1) {
@@ -274,13 +290,13 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
                     return@forEach
                 }
             }
-            if (last is FlashImage && element is PlainText) {
-                if (element == UNSUPPORTED_FLASH_MESSAGE_PLAIN) {
+            // 解决tim发送的语音无法正常识别
+            if (element is PlainText) {
+                if (element == UNSUPPORTED_VOICE_MESSAGE_PLAIN) {
                     last = element
                     return@forEach
                 }
             }
-
             add(element)
             last = element
         }
@@ -431,7 +447,8 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(groupIdOrZero: Long, bot: B
                                     .orEmpty(),
                             proto.pokeType,
                             proto.vaspokeId
-                        ))
+                        )
+                        )
                     }
                     3 -> {
                         val proto = element.commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())

+ 49 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/highway/HighwayHelper.kt

@@ -12,7 +12,10 @@
 package net.mamoe.mirai.qqandroid.network.highway
 
 import io.ktor.client.HttpClient
+import io.ktor.client.request.parameter
+import io.ktor.client.request.port
 import io.ktor.client.request.post
+import io.ktor.client.request.url
 import io.ktor.http.ContentType
 import io.ktor.http.HttpStatusCode
 import io.ktor.http.URLProtocol
@@ -29,8 +32,9 @@ import kotlinx.io.core.use
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.CSDataHighwayHead
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x388
+import net.mamoe.mirai.qqandroid.utils.*
 import net.mamoe.mirai.qqandroid.utils.PlatformSocket
-import net.mamoe.mirai.qqandroid.utils.SocketException
 import net.mamoe.mirai.qqandroid.utils.addSuppressedMirai
 import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.qqandroid.utils.io.withUse
@@ -180,6 +184,50 @@ internal object HighwayHelper {
             }
         }
     }
+
+    suspend fun uploadPttToServers(
+        bot: QQAndroidBot,
+        servers: List<Pair<Int, Int>>,
+        content: ByteArray,
+        md5: ByteArray,
+        uKey: ByteArray, fileKey: ByteArray
+    ) {
+        servers.retryWithServers(10 * 1000, {
+            throw IllegalStateException("cannot upload ptt, failed on all servers.", it)
+        }, { s: String, i: Int ->
+            bot.network.logger.verbose {
+                "[Highway] Uploading ptt to ${s}:$i, size=${content.size.toLong().sizeToString()}"
+            }
+            val time = measureTime {
+                uploadPttToServer(s, i, content, md5, uKey, fileKey)
+            }
+            bot.network.logger.verbose {
+                "[Highway] Uploading ptt: succeed at ${(content.size.toDouble() / 1024 / time.inSeconds).roundToInt()} KiB/s"
+            }
+
+        })
+
+    }
+
+    private suspend fun uploadPttToServer(
+        serverIp: String,
+        serverPort: Int,
+        content: ByteArray,
+        md5: ByteArray,
+        uKey: ByteArray, fileKey: ByteArray
+    ) {
+        MiraiPlatformUtils.Http.post<String> {
+            url("http://$serverIp:$serverPort")
+            parameter("ver", 4679)
+            parameter("ukey", uKey.toUHexString(""))
+            parameter("filekey", fileKey.toUHexString(""))
+            parameter("filesize", content.size)
+            parameter("bmd5", md5.toUHexString(""))
+            parameter("mType", "pttDu")
+            parameter("voice_encodec", 0)
+            body = content
+        }
+    }
 }
 
 

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

@@ -273,7 +273,7 @@ internal class TryUpPttReq(
 internal class TryUpPttRsp(
         @ProtoId(1) @JvmField val fileId: Long = 0L,
         @ProtoId(2) @JvmField val result: Int = 0,
-        @ProtoId(3) @JvmField val failMsg: ByteArray = EMPTY_BYTE_ARRAY,
+        @ProtoId(3) @JvmField val failMsg: ByteArray? = null,
         @ProtoId(4) @JvmField val boolFileExit: Boolean = false,
         @ProtoId(5) @JvmField val uint32UpIp: List<Int>? = null,
         @ProtoId(6) @JvmField val uint32UpPort: List<Int>? = null,

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

@@ -20,6 +20,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.*
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.voice.PttStore
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.ProfileService
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.ConfigPushSvc
@@ -139,6 +140,8 @@ internal object KnownPacketFactories {
         FriendList.GetTroopListSimplify,
         FriendList.GetTroopMemberList,
         ImgStore.GroupPicUp,
+        PttStore.GroupPttUp,
+        PttStore.GroupPttDown,
         LongConn.OffPicUp,
         LongConn.OffPicDown,
         TroopManagement.EditSpecialTitle,

+ 9 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.PbSendMsg.kt

@@ -30,6 +30,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
+import net.mamoe.mirai.qqandroid.utils.hexToBytes
 import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
 import net.mamoe.mirai.qqandroid.utils.io.serialization.toByteArray
 import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
@@ -138,7 +139,14 @@ internal object MessageSvcPbSendMsg : OutgoingPacketFactory<MessageSvcPbSendMsg.
                     richText = ImMsgBody.RichText(
                         elems = message.toRichTextElems(forGroup = true, withGeneralFlags = true),
                         ptt = message.firstOrNull(PttMessage)?.run {
-                            ImMsgBody.Ptt(fileName = fileName.toByteArray(), fileMd5 = md5)
+                            ImMsgBody.Ptt(
+                                fileName = fileName.toByteArray(),
+                                fileMd5 = md5,
+                                boolValid = true,
+                                fileSize = fileSize.toInt(),
+                                fileType = 4,
+                                pbReserve = byteArrayOf(0)
+                            )
                         }
                     )
                 ),

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt


+ 160 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/voice/PttStore.kt

@@ -0,0 +1,160 @@
+package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.voice
+
+import kotlinx.io.core.ByteReadPacket
+import net.mamoe.mirai.qqandroid.EMPTY_BYTE_ARRAY
+import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.network.Packet
+import net.mamoe.mirai.qqandroid.network.QQAndroidClient
+import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x388
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
+import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.getRandomString
+import net.mamoe.mirai.qqandroid.utils._miraiContentToString
+import net.mamoe.mirai.qqandroid.utils.encodeToString
+import net.mamoe.mirai.qqandroid.utils.io.serialization.readProtoBuf
+import net.mamoe.mirai.qqandroid.utils.io.serialization.writeProtoBuf
+import net.mamoe.mirai.qqandroid.utils.toUHexString
+
+internal class PttStore {
+    object GroupPttUp : OutgoingPacketFactory<GroupPttUp.Response>("PttStore.GroupPttUp") {
+
+        sealed class Response : Packet {
+
+            class RequireUpload(
+                val fileId: Long,
+                val uKey: ByteArray,
+                val uploadIpList: List<Int>,
+                val uploadPortList: List<Int>,
+                val fileKey: ByteArray
+            ) : GroupPttUp.Response() {
+                override fun toString(): String {
+                    return "RequireUpload(fileId=$fileId, uKey=${uKey.contentToString()})"
+                }
+            }
+        }
+
+
+        @ExperimentalStdlibApi
+        operator fun invoke(
+            client: QQAndroidClient,
+            uin: Long,
+            groupCode: Long,
+            md5: ByteArray,
+            size: Long,
+            voiceLength: Int,
+            fileId: Long = 0
+        ): OutgoingPacket {
+            val pack = Cmd0x388.ReqBody(
+                netType = 3, // wifi
+                subcmd = 3,
+                msgTryupPttReq = listOf(
+                    Cmd0x388.TryUpPttReq(
+                        srcUin = uin,
+                        groupCode = groupCode,
+                        fileId = fileId,
+                        fileSize = size,
+                        fileMd5 = md5,
+                        fileName = md5,
+                        srcTerm = 5,
+                        platformType = 9,
+                        buType = 4,
+                        innerIp = 0,
+                        buildVer = "6.5.5.663".encodeToByteArray(),
+                        voiceLength = voiceLength,
+                        codec = 0,
+                        voiceType = 1,
+                        boolNewUpChan = true
+                    )
+                )
+            )
+            return buildOutgoingUniPacket(client) {
+                writeProtoBuf(Cmd0x388.ReqBody.serializer(), pack)
+            }
+        }
+
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+            val resp0 = readProtoBuf(Cmd0x388.RspBody.serializer())
+            resp0.msgTryupPttRsp ?: error("cannot find `msgTryupPttRsp` from `Cmd0x388.RspBody`")
+            val resp = resp0.msgTryupPttRsp.first()
+            if (resp.failMsg != null) {
+                throw IllegalStateException(resp.failMsg.encodeToString())
+            }
+            return Response.RequireUpload(
+                fileId = resp.fileid,
+                uKey = resp.upUkey,
+                uploadIpList = resp.uint32UpIp!!,
+                uploadPortList = resp.uint32UpPort!!,
+                fileKey = resp.fileKey
+            )
+
+        }
+
+    }
+
+    object GroupPttDown : OutgoingPacketFactory<GroupPttDown.Response>("PttStore.GroupPttDown") {
+
+        sealed class Response() : Packet {
+            class DownLoadInfo(
+                val downDomain: ByteArray,
+                val downPara:ByteArray,
+                val strDomain:String,
+                val uint32DownIp:List<Int>,
+                val uint32DownPort:List<Int>
+            ) : GroupPttDown.Response() {
+                override fun toString(): String {
+                     return "GroupPttDown(downPara=${downPara.encodeToString()},strDomain=$strDomain})"
+                }
+            }
+
+        }
+
+        @ExperimentalStdlibApi
+        operator fun invoke(
+            client: QQAndroidClient,
+            groupCode: Long,
+            dstUin:Long,
+            md5: ByteArray
+
+        ): OutgoingPacket = buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                Cmd0x388.ReqBody.serializer(), Cmd0x388.ReqBody(
+                    netType = 3, // wifi
+                    subcmd = 4,
+                    msgGetpttUrlReq = listOf(
+                        Cmd0x388.GetPttUrlReq(
+                            groupCode = groupCode,
+                            fileMd5 = md5,
+                            dstUin = dstUin,
+                            buType = 4,
+                            innerIp = 0,
+                            buildVer = "6.5.5.663".encodeToByteArray(),
+                            codec = 0,
+                            reqTerm = 5,
+                            reqPlatformType = 9
+                        )
+                    )
+                )
+            )
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+            val resp0 = readProtoBuf(Cmd0x388.RspBody.serializer())
+            resp0.msgGetpttUrlRsp ?: error("cannot find `msgGetpttUrlRsp` from `Cmd0x388.RspBody`")
+            val resp = resp0.msgGetpttUrlRsp.first()
+            if (!resp.failMsg.contentEquals(EMPTY_BYTE_ARRAY)){
+                throw IllegalStateException(resp.failMsg.encodeToString())
+            }
+            return Response.DownLoadInfo(
+                downDomain = resp.downDomain,
+                downPara = resp.downPara,
+                uint32DownIp = resp.uint32DownIp!!,
+                uint32DownPort = resp.uint32DownPort!!,
+                strDomain = resp.strDomain
+            )
+        }
+    }
+
+}

+ 10 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt

@@ -16,6 +16,7 @@ import net.mamoe.mirai.data.*
 import net.mamoe.mirai.event.events.BotInvitedJoinGroupRequestEvent
 import net.mamoe.mirai.event.events.MemberJoinRequestEvent
 import net.mamoe.mirai.event.events.NewFriendRequestEvent
+import net.mamoe.mirai.message.data.Voice
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
 import net.mamoe.mirai.utils.WeakRef
 
@@ -159,4 +160,13 @@ interface LowLevelBotAPIAccessor {
         blackList: Boolean,
         message: String = ""
     )
+
+    /**
+     * 查询语音的下载连接
+     *
+     * */
+
+    @LowLevelAPI
+    @MiraiExperimentalAPI
+    suspend fun _lowLevelQueryGroupVoiceDownloadUrl(md5: ByteArray, groupId: Long, dstUin: Long): String
 }

+ 4 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Voice.kt

@@ -15,6 +15,7 @@ abstract class PttMessage : MessageContent {
 
     abstract val fileName: String
     abstract val md5: ByteArray
+    abstract val fileSize: Long
 }
 
 
@@ -25,6 +26,7 @@ abstract class PttMessage : MessageContent {
 class Voice(
     override val fileName: String,
     override val md5: ByteArray,
+    override val fileSize: Long,
     private val _url: String
 ) : PttMessage() {
 
@@ -33,9 +35,9 @@ class Voice(
             get() = "Voice"
     }
 
-    val url: String
+    val url: String?
         get() = if (_url.startsWith("http")) _url
-        else "http://grouptalk.c2c.qq.com$_url"
+        else null
 
     private var _stringValue: String? = null
         get() = field ?: kotlin.run {

Vissa filer visades inte eftersom för många filer har ändrats