diff --git a/binary-compatibility-validator/api/binary-compatibility-validator.api b/binary-compatibility-validator/api/binary-compatibility-validator.api index 872d255f3..366d22631 100644 --- a/binary-compatibility-validator/api/binary-compatibility-validator.api +++ b/binary-compatibility-validator/api/binary-compatibility-validator.api @@ -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 diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6cd9fe1cb..c1e8148bd 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -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" diff --git a/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt b/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt new file mode 100644 index 000000000..a93403cee --- /dev/null +++ b/mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt @@ -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" + ) +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt index 01ca79256..e4a98682f 100644 --- a/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt +++ b/mirai-core/src/commonMain/kotlin/contact/GroupSendMessageImpl.kt @@ -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( diff --git a/mirai-core/src/commonMain/kotlin/message/conversions.kt b/mirai-core/src/commonMain/kotlin/message/conversions.kt index 035580c2c..c55e1e0ab 100644 --- a/mirai-core/src/commonMain/kotlin/message/conversions.kt +++ b/mirai-core/src/commonMain/kotlin/message/conversions.kt @@ -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() }) { diff --git a/mirai-core/src/commonMain/kotlin/message/lightApp.kt b/mirai-core/src/commonMain/kotlin/message/lightApp.kt new file mode 100644 index 000000000..f50ed1673 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/message/lightApp.kt @@ -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 = "" + ) + } +} diff --git a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt index 7791f9138..57571b8da 100644 --- a/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt +++ b/mirai-core/src/commonMain/kotlin/message/outgoingSourceImpl.kt @@ -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 diff --git a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt index a5e6f682e..7730ad4ea 100644 --- a/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt +++ b/mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt @@ -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 diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt index 4ca282e55..c2fbf5276 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/MsgCommon.kt @@ -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, diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt index a964025f7..34331649c 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt @@ -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 diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt new file mode 100644 index 000000000..2ec8f4268 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OidbCmd0xb77.kt @@ -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 +} + \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt index df64482d9..7196f09ff 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt @@ -157,6 +157,7 @@ internal object KnownPacketFactories { StrangerList.GetStrangerList, StrangerList.DelStranger, SummaryCard.ReqSummaryCard, + MusicSharePacket, ) object IncomingFactories : List<IncomingPacketFactory<*>> by mutableListOf( diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt new file mode 100644 index 000000000..5559e2fc4 --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/MusicSharePacket.kt @@ -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()) + ) + ) + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt new file mode 100644 index 000000000..3ca15eefc --- /dev/null +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/SendMessageMultiProtocol.kt @@ -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) + } +} \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt index 2db861df0..88b34c54e 100644 --- a/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt +++ b/mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt @@ -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 diff --git a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt index 51436ddda..ae71df0fb 100644 --- a/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt +++ b/mirai-core/src/commonMain/kotlin/utils/io/serialization/utils.kt @@ -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 */