1
0
mirror of https://github.com/mamoe/mirai.git synced 2025-04-05 07:10:11 +08:00

支持使用QQ互联通道发送音乐分享 ()

* Add new MessageContent MusicShare and its protocol internals. , 

* MusicShare sending

* MusicShare: MessageSource integration fundamentals

* - MessageReceipt integration for MusicShare
- Support MusicShare in QuoteReply: transform as PlainText
- Support LightApp refining, support decoding MusicShare

* Dump api for MusicShare

* Remove debugging code

* 2.1.0-dev-3

* Remove confusing providedSequenceIds in OnlineMessageSourceToGroupImpl

* fix build

Co-authored-by: wdvxdr <wdvxdr@foxmail.com>
This commit is contained in:
Him188 2021-01-23 22:09:27 +08:00 committed by GitHub
parent 705153c7d6
commit 2e5b223b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 753 additions and 56 deletions
binary-compatibility-validator/api
buildSrc/src/main/kotlin
mirai-core-api/src/commonMain/kotlin/message/data
mirai-core/src/commonMain/kotlin

View File

@ -4661,6 +4661,41 @@ public final class net/mamoe/mirai/message/data/MessageUtils {
public static final synthetic fun toPlainText (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/PlainText;
}
public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent {
public static final field Key Lnet/mamoe/mirai/message/data/MusicShare$Key;
public fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Lnet/mamoe/mirai/message/data/MusicType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun contentToString ()Ljava/lang/String;
public fun equals (Ljava/lang/Object;)Z
public final fun getBrief ()Ljava/lang/String;
public final fun getJumpUrl ()Ljava/lang/String;
public fun getKey ()Lnet/mamoe/mirai/message/data/MessageKey;
public final fun getMusicUrl ()Ljava/lang/String;
public final fun getPictureUrl ()Ljava/lang/String;
public final fun getSummary ()Ljava/lang/String;
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lnet/mamoe/mirai/message/data/MusicType;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class net/mamoe/mirai/message/data/MusicShare$Key : net/mamoe/mirai/message/data/AbstractPolymorphicMessageKey {
}
public final class net/mamoe/mirai/message/data/MusicType : java/lang/Enum {
public static final field MiguMusic Lnet/mamoe/mirai/message/data/MusicType;
public static final field NeteaseCloudMusic Lnet/mamoe/mirai/message/data/MusicType;
public static final field QQMusic Lnet/mamoe/mirai/message/data/MusicType;
public final fun getAppId ()J
public final fun getPackageName ()Ljava/lang/String;
public final fun getPlatform ()I
public final fun getSdkVersion ()Ljava/lang/String;
public final fun getSignature ()Ljava/lang/String;
public static fun valueOf (Ljava/lang/String;)Lnet/mamoe/mirai/message/data/MusicType;
public static fun values ()[Lnet/mamoe/mirai/message/data/MusicType;
}
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

View File

@ -12,7 +12,7 @@
import org.gradle.api.attributes.Attribute
object Versions {
const val project = "2.1.0-dev-2"
const val project = "2.1.0-dev-3"
const val kotlinCompiler = "1.4.21"
const val kotlinStdlib = "1.4.21"

View File

@ -0,0 +1,132 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused")
package net.mamoe.mirai.message.data
import net.mamoe.mirai.utils.MiraiExperimentalApi
import net.mamoe.mirai.utils.MiraiInternalApi
import net.mamoe.mirai.utils.safeCast
/**
* QQ 互联通道音乐分享
*
* @since 2.1
*/
@MiraiExperimentalApi
public class MusicShare @JvmOverloads constructor(
/**
* 音乐应用类型
*/
public val type: MusicType,
/**
* 消息卡片标题
*/
public val title: String,
/**
* 消息卡片内容
*/
public val summary: String,
/**
* 点击卡片跳转网页 URL
*/
public val jumpUrl: String,
/**
* 消息卡片图片 URL
*/
public val pictureUrl: String,
/**
* 音乐文件 URL
*/
public val musicUrl: String,
/**
* 在消息列表显示
*/
public val brief: String = "[分享]$title",
) : MessageContent, ConstrainSingle {
override val key: MessageKey<*> get() = Key
override fun contentToString(): String =
brief.takeIf { it.isNotBlank() } ?: "[分享]$title" // empty content is not accepted by `sendMessage`
// MusicShare(type=NeteaseCloudMusic, title='ファッション', summary='rinahamu/Yunomi', brief='', url='http://music.163.com/song/1338728297/?userid=324076307', pictureUrl='http://p2.music.126.net/y19E5SadGUmSR8SZxkrNtw==/109951163785855539.jpg', musicUrl='http://music.163.com/song/media/outer/url?id=1338728297&userid=324076307')
override fun toString(): String {
return "MusicShare(type=$type, title='$title', summary='$summary', brief='$brief', url='$jumpUrl', pictureUrl='$pictureUrl', musicUrl='$musicUrl')"
}
// don't make this class 'data' unless we made it stable.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MusicShare
if (type != other.type) return false
if (title != other.title) return false
if (summary != other.summary) return false
if (brief != other.brief) return false
if (jumpUrl != other.jumpUrl) return false
if (pictureUrl != other.pictureUrl) return false
if (musicUrl != other.musicUrl) return false
return true
}
override fun hashCode(): Int {
var result = type.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + summary.hashCode()
result = 31 * result + brief.hashCode()
result = 31 * result + jumpUrl.hashCode()
result = 31 * result + pictureUrl.hashCode()
result = 31 * result + musicUrl.hashCode()
return result
}
public companion object Key :
AbstractPolymorphicMessageKey<MessageContent, MusicShare>(MessageContent, { it.safeCast() })
}
/**
* @since 2.1
*/
public enum class MusicType constructor(
@MiraiInternalApi public val appId: Long,
@MiraiInternalApi public val platform: Int,
@MiraiInternalApi public val sdkVersion: String,
@MiraiInternalApi public val packageName: String,
@MiraiInternalApi public val signature: String
) {
NeteaseCloudMusic(
100495085,
1,
"0.0.0",
"com.netease.cloudmusic",
"da6b069da1e2982db3e386233f68d76d"
),
QQMusic(
100497308,
1,
"0.0.0",
"com.tencent.qqmusic",
"cbd27cd7c861227d013a25b2d10f0799"
),
MiguMusic(
1101053067,
1,
"0.0.0",
"cmccwm.mobilemusic",
"6cdc72a439cef99a3418d2a78aa28c73"
)
}

View File

@ -17,13 +17,17 @@ import net.mamoe.mirai.contact.MessageTooLargeException
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.EventCancelledException
import net.mamoe.mirai.event.events.GroupMessagePreSendEvent
import net.mamoe.mirai.event.nextEventOrNull
import net.mamoe.mirai.internal.MiraiImpl
import net.mamoe.mirai.internal.forwardMessage
import net.mamoe.mirai.internal.longMessage
import net.mamoe.mirai.internal.message.*
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.protocol.packet.chat.MusicSharePacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.SendMessageMultiProtocol
import net.mamoe.mirai.internal.network.protocol.packet.chat.image.ImgStore
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.OnlinePushPbPushGroupMsg
import net.mamoe.mirai.message.MessageReceipt
import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.utils.currentTimeSeconds
@ -111,61 +115,83 @@ private suspend fun GroupImpl.sendMessagePacket(
val group = this
val source: OnlineMessageSourceToGroupImpl
var source: OnlineMessageSourceToGroupImpl? = null
bot.network.run {
MessageSvcPbSendMsg.createToGroup(
bot.client,
group,
finalMessage,
SendMessageMultiProtocol.createToGroup(
bot.client, group, finalMessage,
step == GroupMessageSendingStep.FRAGMENTED
) { source = it }.forEach { packet ->
packet.sendAndExpect<MessageSvcPbSendMsg.Response>().let { resp ->
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return when (step) {
GroupMessageSendingStep.FIRST -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.LONG_MESSAGE
)
}
GroupMessageSendingStep.LONG_MESSAGE -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.FRAGMENTED
)
}
else -> {
throw MessageTooLargeException(
group,
originalMessage,
finalMessage,
"Message '${finalMessage.content.take(10)}' is too large."
)
}
}.getOrThrow()
when (val resp = packet.sendAndExpect<Packet>()) {
is MessageSvcPbSendMsg.Response -> {
if (resp is MessageSvcPbSendMsg.Response.MessageTooLarge) {
return when (step) {
GroupMessageSendingStep.FIRST -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.LONG_MESSAGE
)
}
GroupMessageSendingStep.LONG_MESSAGE -> {
sendMessageImpl(
originalMessage,
transformedMessage,
GroupMessageSendingStep.FRAGMENTED
)
}
else -> {
throw MessageTooLargeException(
group,
originalMessage,
finalMessage,
"Message '${finalMessage.content.take(10)}' is too large."
)
}
}.getOrThrow()
}
check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send group message failed: $resp"
}
}
check(resp is MessageSvcPbSendMsg.Response.SUCCESS) {
"Send group message failed: $resp"
is MusicSharePacket.Response -> {
resp.pkg.checkSuccess("send group music share")
val receipt: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt =
nextEventOrNull(3000) { it.fromAppId == 3116 }
?: OnlinePushPbPushGroupMsg.SendGroupMessageReceipt.EMPTY
source = OnlineMessageSourceToGroupImpl(
group,
internalIds = intArrayOf(receipt.messageRandom),
providedSequenceIds = intArrayOf(receipt.sequenceId),
sender = bot,
target = group,
time = currentTimeSeconds().toInt(),
originalMessage = finalMessage
)
}
}
}
check(source != null) {
"Internal error: source is not initialized"
}
try {
source!!.ensureSequenceIdAvailable()
} catch (e: Exception) {
bot.network.logger.warning(
"Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
e
)
}
return MessageReceipt(source!!, group)
}
try {
source.ensureSequenceIdAvailable()
} catch (e: Exception) {
bot.network.logger.warning(
"Timeout awaiting sequenceId for group message(${finalMessage.content.take(10)}). Some features may not work properly",
e
)
}
return MessageReceipt(source, group)
}
private suspend fun GroupImpl.uploadGroupLongMessageHighway(

View File

@ -232,6 +232,12 @@ internal fun MessageChain.toRichTextElems(
)
)
}
is MusicShare -> {
// 只有在 QuoteReply 的 source 里才会进行 MusicShare 转换, 因此可以转 PT.
// 发送消息时会被特殊处理
transformOneMessage(PlainText(currentMessage.content))
}
is ForwardMessage,
is MessageSource, // mirai metadata only
is RichMessage // already transformed above
@ -510,7 +516,8 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(
else -> error("unknown compression flag=${element.lightApp.data[0]}")
}
}
list.add(LightApp(content))
list.add(LightApp(content).refine())
}
element.richMsg != null -> {
val content = runWithBugReport("解析 richMsg", { element.richMsg.template1.toUHexString() }) {

View File

@ -0,0 +1,172 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.message
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import net.mamoe.mirai.message.data.LightApp
import net.mamoe.mirai.message.data.MusicShare
import net.mamoe.mirai.message.data.MusicType
import net.mamoe.mirai.message.data.SingleMessage
private val json = Json {
ignoreUnknownKeys = true
}
internal fun LightApp.tryDeserialize(): LightAppStruct? {
return kotlin.runCatching {
json.decodeFromString(LightAppStruct.serializer(), this.content)
}.getOrNull()
}
/**
* 识别 app 内容, 如果有必要
*/
internal fun LightApp.refine(): SingleMessage {
val struct = tryDeserialize() ?: return this
struct.run {
if (meta.music != null) {
MusicType.values().find { it.appId.toInt() == meta.music.appid }?.let { musicType ->
meta.music.run {
return MusicShare(
type = musicType, title = title, summary = desc,
jumpUrl = jumpUrl, pictureUrl = preview, musicUrl = musicUrl, brief = prompt
)
}
}
}
}
return this
}
/*
EXAMPLE LightAppStruct for MusicShare
{
"app": "com.tencent.structmsg",
"config": {
"autosize": true,
"ctime": 1611339208,
"forward": true,
"token": "1f27c2b5687e0320549992a4652c8465",
"type": "normal"
},
"desc": "音乐",
"extra": {
"app_type": 1,
"appid": 100495085, // NeteaseCloudMusic
"uin": 123456789 // qq uin
},
"meta": {
"music": {
"action": "",
"android_pkg_name": "",
"app_type": 1,
"appid": 100495085,
"desc": "rinahamu/Yunomi",
"jumpUrl": "http://music.163.com/song/1338728297/?userid=324076307",
"musicUrl": "http://music.163.com/song/media/outer/url?id=1338728297&userid=324076307",
"preview": "http://p2.music.126.net/y19E5SadGUmSR8SZxkrNtw==/109951163785855539.jpg",
"sourceMsgId": "0",
"source_icon": "",
"source_url": "",
"tag": "网易云音乐",
"title": "ファッション"
}
},
"prompt": "[分享]ファッション",
"ver": "0.0.0.1",
"view": "music"
}
*/
@Serializable
internal data class LightAppStruct(
@SerialName("app")
val app: String = "",
@SerialName("config")
val config: Config = Config(),
@SerialName("desc")
val desc: String = "",
@SerialName("extra")
val extra: Extra = Extra(),
@SerialName("meta")
val meta: Meta = Meta(),
@SerialName("prompt")
val prompt: String = "",
@SerialName("ver")
val ver: String = "",
@SerialName("view")
val view: String = ""
) {
@Serializable
data class Config(
@SerialName("autosize")
val autosize: Boolean = false,
@SerialName("ctime")
val ctime: Int = 0,
@SerialName("forward")
val forward: Boolean = false,
@SerialName("token")
val token: String = "",
@SerialName("type")
val type: String = ""
)
@Serializable
data class Extra(
@SerialName("app_type")
val appType: Int = 0,
@SerialName("appid")
val appid: Int = 0,
@SerialName("uin")
val uin: Int = 0
)
@Serializable
data class Meta(
@SerialName("music")
val music: Music? = null
) {
@Serializable
data class Music(
@SerialName("action")
val action: String = "",
@SerialName("android_pkg_name")
val androidPkgName: String = "",
@SerialName("app_type")
val appType: Int = 0,
@SerialName("appid")
val appid: Int = 0,
@SerialName("desc")
val desc: String = "",
@SerialName("jumpUrl")
val jumpUrl: String = "",
@SerialName("musicUrl")
val musicUrl: String = "",
@SerialName("preview")
val preview: String = "",
@SerialName("source_icon")
val sourceIcon: String = "",
@SerialName("sourceMsgId")
val sourceMsgId: String = "",
@SerialName("source_url")
val sourceUrl: String = "",
@SerialName("tag")
val tag: String = "",
@SerialName("title")
val title: String = ""
)
}
}

View File

@ -11,6 +11,7 @@
package net.mamoe.mirai.internal.message
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -136,11 +137,12 @@ internal class OnlineMessageSourceToTempImpl(
@Serializable(OnlineMessageSourceToGroupImpl.Serializer::class)
internal class OnlineMessageSourceToGroupImpl(
coroutineScope: CoroutineScope,
override val internalIds: IntArray,
override val internalIds: IntArray, // aka random
override val time: Int,
override val originalMessage: MessageChain,
override val sender: Bot,
override val target: Group
override val target: Group,
providedSequenceIds: IntArray? = null,
) : OnlineMessageSource.Outgoing.ToGroup(), MessageSourceInternal {
object Serializer : MessageSourceSerializerImpl("OnlineMessageSourceToGroup")
@ -150,7 +152,7 @@ internal class OnlineMessageSourceToGroupImpl(
get() = sender
override var isRecalledOrPlanned: AtomicBoolean = AtomicBoolean(false)
private val sequenceIdDeferred: Deferred<IntArray?> = run {
private val sequenceIdDeferred: Deferred<IntArray?> = providedSequenceIds?.let { CompletableDeferred(it) } ?: run {
val multi = mutableMapOf<Int, Int>()
coroutineScope.asyncFromEventOrNull<SendGroupMessageReceipt, IntArray>(
timeoutMillis = 3000L * this@OnlineMessageSourceToGroupImpl.internalIds.size

View File

@ -162,6 +162,7 @@ internal open class QQAndroidClient(
val apkVersionName: ByteArray get() = protocol.ver.toByteArray() //"8.4.18".toByteArray()
val buildVer: String get() = "8.4.18.4810" // 8.2.0.1296 // 8.4.8.4810 // 8.2.7.4410
val clientVersion: String = "android ${protocol.ver}" // android 8.5.0
val buildTime: Long get() = protocol.buildTime
val sdkVersion: String get() = protocol.sdkVer

View File

@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
@ -95,6 +95,13 @@ internal class MsgComm : ProtoBuf {
@ProtoNumber(7) var msgUid: Long = 0L,
@ProtoNumber(8) @JvmField val c2cTmpMsgHead: C2CTmpMsgHead? = null,
@ProtoNumber(9) @JvmField val groupInfo: GroupInfo? = null,
/**
* 1: 群消息 by pc tim
* 1001: 群消息 sent by android phone
*
*
* 3116: music share, ANDROID_PHONE 发送
*/
@ProtoNumber(10) @JvmField val fromAppid: Int = 0,
@ProtoNumber(11) @JvmField val fromInstid: Int = 0,
@ProtoNumber(12) @JvmField val userActive: Int = 0,

View File

@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.network.protocol.data.proto
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.protocol.packet.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.internal.utils.io.ProtoBuf
@ -962,7 +963,13 @@ internal class OidbSso : ProtoBuf {
@ProtoNumber(4) @JvmField val bodybuffer: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) @JvmField val errorMsg: String = "",
@ProtoNumber(6) @JvmField val clientVersion: String = ""
) : ProtoBuf
) : ProtoBuf, Packet {
fun checkSuccess(actionName: String) {
check(result == 0) {
"${actionName.capitalize()} failed. result=$result, errorMsg=$errorMsg"
}
}
}
}
@Serializable

View File

@ -0,0 +1,157 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
@file:Suppress("unused", "SpellCheckingInspection")
package net.mamoe.mirai.internal.network.protocol.data.proto
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.utils.io.ProtoBuf
@Serializable
internal class OidbCmd0xb77 : ProtoBuf {
@Serializable
internal class ArkJsonBody(
@JvmField @ProtoNumber(10) val jsonStr: String = ""
) : ProtoBuf
@Serializable
internal class ArkMsgBody(
@JvmField @ProtoNumber(1) val app: String = "",
@JvmField @ProtoNumber(2) val view: String = "",
@JvmField @ProtoNumber(3) val prompt: String = "",
@JvmField @ProtoNumber(4) val ver: String = "",
@JvmField @ProtoNumber(5) val desc: String = "",
@JvmField @ProtoNumber(6) val featureId: Int = 0,
@JvmField @ProtoNumber(10) val meta: String = "",
@JvmField @ProtoNumber(11) val metaUrl1: String = "",
@JvmField @ProtoNumber(12) val metaUrl2: String = "",
@JvmField @ProtoNumber(13) val metaUrl3: String = "",
@JvmField @ProtoNumber(14) val metaText1: String = "",
@JvmField @ProtoNumber(15) val metaText2: String = "",
@JvmField @ProtoNumber(16) val metaText3: String = "",
@JvmField @ProtoNumber(20) val config: String = ""
) : ProtoBuf
@Serializable
internal class ArkV1MsgBody(
@JvmField @ProtoNumber(1) val app: String = "",
@JvmField @ProtoNumber(2) val view: String = "",
@JvmField @ProtoNumber(3) val prompt: String = "",
@JvmField @ProtoNumber(4) val ver: String = "",
@JvmField @ProtoNumber(5) val desc: String = "",
@JvmField @ProtoNumber(6) val featureId: Int = 0,
@JvmField @ProtoNumber(10) val meta: String = "",
@JvmField @ProtoNumber(11) val items: List<TemplateItem> = emptyList(),
@JvmField @ProtoNumber(20) val config: String = ""
) : ProtoBuf
@Serializable
internal class ClientInfo(
@JvmField @ProtoNumber(1) val platform: Int = 0,
@JvmField @ProtoNumber(2) val sdkVersion: String = "",
@JvmField @ProtoNumber(3) val androidPackageName: String = "",
@JvmField @ProtoNumber(4) val androidSignature: String = "",
@JvmField @ProtoNumber(5) val iosBundleId: String = "",
@JvmField @ProtoNumber(6) val pcSign: String = ""
) : ProtoBuf
@Serializable
internal class ImageInfo(
@JvmField @ProtoNumber(1) val md5: String = "",
@JvmField @ProtoNumber(2) val uuid: String = "",
@JvmField @ProtoNumber(3) val imgType: Int = 0,
@JvmField @ProtoNumber(4) val fileSize: Int = 0,
@JvmField @ProtoNumber(5) val width: Int = 0,
@JvmField @ProtoNumber(6) val height: Int = 0,
@JvmField @ProtoNumber(7) val original: Int = 0,
@JvmField @ProtoNumber(101) val fileId: Int = 0,
@JvmField @ProtoNumber(102) val serverIp: Int = 0,
@JvmField @ProtoNumber(103) val serverPort: Int = 0
) : ProtoBuf
@Serializable
internal class MiniAppMsgBody(
@JvmField @ProtoNumber(1) val miniAppAppid: Long = 0L,
@JvmField @ProtoNumber(2) val miniAppPath: String = "",
@JvmField @ProtoNumber(3) val webPageUrl: String = "",
@JvmField @ProtoNumber(4) val miniAppType: Int = 0,
@JvmField @ProtoNumber(5) val title: String = "",
@JvmField @ProtoNumber(6) val desc: String = "",
@JvmField @ProtoNumber(10) val jsonStr: String = ""
) : ProtoBuf
@Serializable
internal class ReqBody(
@JvmField @ProtoNumber(1) val appid: Long = 0L,
@JvmField @ProtoNumber(2) val appType: Int = 0,
@JvmField @ProtoNumber(3) val msgStyle: Int = 0,
@JvmField @ProtoNumber(4) val senderUin: Long = 0L,
@JvmField @ProtoNumber(5) val clientInfo: ClientInfo? = null,
// @JvmField @ProtoNumber(6) val textMsg: String? = null,
@JvmField @ProtoNumber(7) val extInfo: ExtInfo? = null,
@JvmField @ProtoNumber(10) val sendType: Int = 0,
@JvmField @ProtoNumber(11) val recvUin: Long = 0L,
@JvmField @ProtoNumber(12) val richMsgBody: RichMsgBody? = null,
@JvmField @ProtoNumber(13) val arkMsgBody: ArkMsgBody? = null,
// @JvmField @ProtoNumber(14) val recvOpenid: String? = null, // don't be ""
@JvmField @ProtoNumber(15) val arkv1MsgBody: ArkV1MsgBody? = null,
@JvmField @ProtoNumber(16) val arkJsonBody: ArkJsonBody? = null,
@JvmField @ProtoNumber(17) val xmlMsgBody: XmlMsgBody? = null,
@JvmField @ProtoNumber(18) val miniAppMsgBody: MiniAppMsgBody? = null
) : ProtoBuf
@Serializable
internal class ExtInfo(
@ProtoNumber(1) @JvmField val customFeatureId: List<Int> = emptyList(),
@ProtoNumber(2) @JvmField val apnsWording: String = "",
@ProtoNumber(3) @JvmField val groupSaveDbFlag: Int = 0,
@ProtoNumber(4) @JvmField val receiverAppId: Int = 0,
@ProtoNumber(5) @JvmField val msgSeq: Long = 0L,
) : ProtoBuf
@Serializable
internal class RichMsgBody(
@JvmField @ProtoNumber(1) val usingArk: Boolean = false,
@JvmField @ProtoNumber(10) val title: String = "",
@JvmField @ProtoNumber(11) val summary: String = "",
@JvmField @ProtoNumber(12) val brief: String = "",
@JvmField @ProtoNumber(13) val url: String = "",
@JvmField @ProtoNumber(14) val pictureUrl: String = "",
@JvmField @ProtoNumber(15) val action: String = "",
@JvmField @ProtoNumber(16) val musicUrl: String = "",
@JvmField @ProtoNumber(21) val imageInfo: ImageInfo? = null
) : ProtoBuf
@Serializable
internal class RspBody(
@JvmField @ProtoNumber(1) val wording: String = "",
@JvmField @ProtoNumber(2) val jumpResult: Int = 0,
@JvmField @ProtoNumber(3) val jumpUrl: String = "",
@JvmField @ProtoNumber(4) val level: Int = 0,
@JvmField @ProtoNumber(5) val subLevel: Int = 0,
@JvmField @ProtoNumber(6) val developMsg: String = ""
) : ProtoBuf, Packet
@Serializable
internal class TemplateItem(
@JvmField @ProtoNumber(1) val key: String = "",
@JvmField @ProtoNumber(2) val type: Int = 0,
@JvmField @ProtoNumber(3) val value: String = ""
) : ProtoBuf
@Serializable
internal class XmlMsgBody(
@JvmField @ProtoNumber(11) val serviceId: Int = 0,
@JvmField @ProtoNumber(12) val xml: String = ""
) : ProtoBuf
}

View File

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

View File

@ -0,0 +1,91 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.packet.chat
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.network.Packet
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.data.proto.OidbCmd0xb77
import net.mamoe.mirai.internal.network.protocol.data.proto.OidbSso
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
import net.mamoe.mirai.message.data.MessageSourceKind
import net.mamoe.mirai.message.data.MusicShare
internal object MusicSharePacket :
OutgoingPacketFactory<MusicSharePacket.Response>("OidbSvc.0xb77_9") {
class Response(
val pkg: OidbSso.OIDBSSOPkg,
) : Packet {
val response by lazy {
pkg.bodybuffer.loadAs(OidbCmd0xb77.RspBody.serializer())
}
override fun toString(): String =
"MusicSharePacket.Response(success=${pkg.result == 0}, error=${pkg.errorMsg})"
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
return Response(readProtoBuf(OidbSso.OIDBSSOPkg.serializer()))
}
operator fun invoke(
client: QQAndroidClient,
musicShare: MusicShare,
targetUin: Long,
targetKind: MessageSourceKind
) = buildOutgoingUniPacket(client) {
with(musicShare) {
val musicType = musicShare.type
writeProtoBuf(
OidbSso.OIDBSSOPkg.serializer(),
OidbSso.OIDBSSOPkg(
command = 2935,
serviceType = 9,
clientVersion = client.clientVersion,
bodybuffer = OidbCmd0xb77.ReqBody(
appid = musicType.appId,
appType = 1,
msgStyle = if (jumpUrl.isNotBlank()) 4 else 0, // 有播放连接为4, 无播放连接为0
clientInfo = OidbCmd0xb77.ClientInfo(
platform = musicType.platform,
sdkVersion = musicType.sdkVersion,
androidPackageName = musicType.packageName,
androidSignature = musicType.signature
),
extInfo = OidbCmd0xb77.ExtInfo(
msgSeq = 0
),
sendType = when (targetKind) {
MessageSourceKind.FRIEND -> 0
MessageSourceKind.GROUP -> 1
else -> error("Internal error: Unsupported targetKind $targetKind")
},
recvUin = targetUin,
richMsgBody = OidbCmd0xb77.RichMsgBody(
title = title,
summary = summary,
brief = brief,
url = jumpUrl,
pictureUrl = pictureUrl,
musicUrl = musicUrl
)
).toByteArray(OidbCmd0xb77.ReqBody.serializer())
)
)
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.packet.chat
import net.mamoe.mirai.contact.Group
import net.mamoe.mirai.internal.contact.takeSingleContent
import net.mamoe.mirai.internal.contact.uin
import net.mamoe.mirai.internal.message.OnlineMessageSourceToGroupImpl
import net.mamoe.mirai.internal.network.QQAndroidClient
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacket
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbSendMsg
import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.createToGroup
import net.mamoe.mirai.message.data.MessageChain
import net.mamoe.mirai.message.data.MessageSourceKind
import net.mamoe.mirai.message.data.MusicShare
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
internal object SendMessageMultiProtocol {
inline fun createToGroup(
client: QQAndroidClient,
group: Group,
message: MessageChain,
fragmented: Boolean,
crossinline sourceCallback: (OnlineMessageSourceToGroupImpl) -> Unit
): List<OutgoingPacket> {
contract { callsInPlace(sourceCallback, InvocationKind.AT_MOST_ONCE) }
message.takeSingleContent<MusicShare>()?.let { musicShare ->
return listOf(MusicSharePacket(client, musicShare, group.uin, targetKind = MessageSourceKind.GROUP))
}
return MessageSvcPbSendMsg.createToGroup(client, group, message, fragmented, sourceCallback)
}
}

View File

@ -42,11 +42,16 @@ import net.mamoe.mirai.utils.*
internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("OnlinePush.PbPushGroupMsg") {
internal class SendGroupMessageReceipt(
val messageRandom: Int,
val sequenceId: Int
val sequenceId: Int,
val fromAppId: Int,
) : Packet, Event, Packet.NoLog, AbstractEvent() {
override fun toString(): String {
return "OnlinePush.PbPushGroupMsg.SendGroupMessageReceipt(messageRandom=$messageRandom, sequenceId=$sequenceId)"
}
companion object {
val EMPTY = SendGroupMessageReceipt(0, 0, 0)
}
}
@OptIn(ExperimentalStdlibApi::class)
@ -61,11 +66,13 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
if (isFromSelfAccount) {
val messageRandom = pbPushMsg.msg.msgBody.richText.attr?.random ?: return null
if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom }) {
if (bot.client.syncingController.pendingGroupMessageReceiptCacheList.contains { it.messageRandom == messageRandom }
|| msgHead.fromAppid == 3116) {
// message sent by bot
return SendGroupMessageReceipt(
messageRandom,
msgHead.msgSeq
msgHead.msgSeq,
msgHead.fromAppid
)
}
// else: sync form other device

View File

@ -19,9 +19,11 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import net.mamoe.mirai.internal.network.protocol.data.jce.RequestDataVersion2
import net.mamoe.mirai.internal.network.protocol.data.jce.RequestDataVersion3
import net.mamoe.mirai.internal.network.protocol.data.jce.RequestPacket
import net.mamoe.mirai.internal.network.protocol.data.proto.OidbSso
import net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.ProtoBuf
import net.mamoe.mirai.internal.utils.io.serialization.tars.Tars
import net.mamoe.mirai.internal.utils.soutv
import net.mamoe.mirai.utils.read
import net.mamoe.mirai.utils.readPacketExact
import kotlin.contracts.InvocationKind
@ -139,6 +141,14 @@ internal fun <T : ProtoBuf> ByteArray.loadAs(deserializer: DeserializationStrate
return KtProtoBuf.decodeFromByteArray(deserializer, this)
}
internal fun <T : ProtoBuf> ByteArray.loadOidb(deserializer: DeserializationStrategy<T>, log: Boolean = false): T {
val oidb = loadAs(OidbSso.OIDBSSOPkg.serializer())
if (log) {
oidb.soutv("OIDB")
}
return oidb.bodybuffer.loadAs(deserializer)
}
/**
* load
*/