ReceiveMessageHandler.kt 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. /*
  2. * Copyright 2020 Mamoe Technologies and contributors.
  3. *
  4. * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
  5. * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
  6. *
  7. * https://github.com/mamoe/mirai/blob/master/LICENSE
  8. */
  9. package net.mamoe.mirai.internal.message
  10. import kotlinx.io.core.discardExact
  11. import kotlinx.io.core.readUInt
  12. import kotlinx.io.core.readUShort
  13. import kotlinx.serialization.json.Json
  14. import net.mamoe.mirai.Bot
  15. import net.mamoe.mirai.internal.message.DeepMessageRefiner.refineDeep
  16. import net.mamoe.mirai.internal.message.LightMessageRefiner.refineLight
  17. import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.cleanupRubbishMessageElements
  18. import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.joinToMessageChain
  19. import net.mamoe.mirai.internal.message.ReceiveMessageTransformer.toAudio
  20. import net.mamoe.mirai.internal.network.protocol.data.proto.*
  21. import net.mamoe.mirai.internal.utils.io.serialization.loadAs
  22. import net.mamoe.mirai.internal.utils.io.serialization.readProtoBuf
  23. import net.mamoe.mirai.message.data.*
  24. import net.mamoe.mirai.utils.read
  25. import net.mamoe.mirai.utils.toLongUnsigned
  26. import net.mamoe.mirai.utils.toUHexString
  27. import net.mamoe.mirai.utils.unzip
  28. /**
  29. * 只在手动构造 [OfflineMessageSource] 时调用
  30. */
  31. internal fun ImMsgBody.SourceMsg.toMessageChainNoSource(
  32. bot: Bot,
  33. messageSourceKind: MessageSourceKind,
  34. groupIdOrZero: Long,
  35. refineContext: RefineContext = EmptyRefineContext,
  36. ): MessageChain {
  37. val elements = this.elems
  38. return buildMessageChain(elements.size + 1) {
  39. joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, this)
  40. }.cleanupRubbishMessageElements().refineLight(bot, refineContext)
  41. }
  42. internal suspend fun List<MsgComm.Msg>.toMessageChainOnline(
  43. bot: Bot,
  44. groupIdOrZero: Long,
  45. messageSourceKind: MessageSourceKind,
  46. refineContext: RefineContext = EmptyRefineContext,
  47. ): MessageChain {
  48. return toMessageChain(bot, groupIdOrZero, true, messageSourceKind).refineDeep(bot, refineContext)
  49. }
  50. internal suspend fun MsgComm.Msg.toMessageChainOnline(
  51. bot: Bot,
  52. refineContext: RefineContext = EmptyRefineContext,
  53. ): MessageChain {
  54. fun getSourceKind(c2cCmd: Int): MessageSourceKind {
  55. return when (c2cCmd) {
  56. 11 -> MessageSourceKind.FRIEND // bot 给其他人发消息
  57. 4 -> MessageSourceKind.FRIEND // bot 给自己作为好友发消息 (非 other client)
  58. 1 -> MessageSourceKind.GROUP
  59. else -> error("Could not get source kind from c2cCmd: $c2cCmd")
  60. }
  61. }
  62. val kind = getSourceKind(msgHead.c2cCmd)
  63. val groupId = when (kind) {
  64. MessageSourceKind.GROUP -> msgHead.groupInfo?.groupCode ?: 0
  65. else -> 0
  66. }
  67. return listOf(this).toMessageChainOnline(bot, groupId, kind, refineContext)
  68. }
  69. //internal fun List<MsgComm.Msg>.toMessageChainOffline(
  70. // bot: Bot,
  71. // groupIdOrZero: Long,
  72. // messageSourceKind: MessageSourceKind
  73. //): MessageChain {
  74. // return toMessageChain(bot, groupIdOrZero, false, messageSourceKind).refineLight(bot)
  75. //}
  76. internal fun List<MsgComm.Msg>.toMessageChainNoSource(
  77. bot: Bot,
  78. groupIdOrZero: Long,
  79. messageSourceKind: MessageSourceKind,
  80. refineContext: RefineContext = EmptyRefineContext,
  81. ): MessageChain {
  82. return toMessageChain(bot, groupIdOrZero, null, messageSourceKind).refineLight(bot, refineContext)
  83. }
  84. private fun List<MsgComm.Msg>.toMessageChain(
  85. bot: Bot,
  86. groupIdOrZero: Long,
  87. onlineSource: Boolean?,
  88. messageSourceKind: MessageSourceKind,
  89. ): MessageChain {
  90. val messageList = this
  91. val elements = messageList.flatMap { it.msgBody.richText.elems }
  92. val builder = MessageChainBuilder(elements.size)
  93. if (onlineSource != null) {
  94. builder.add(ReceiveMessageTransformer.createMessageSource(bot, onlineSource, messageSourceKind, messageList))
  95. }
  96. joinToMessageChain(elements, groupIdOrZero, messageSourceKind, bot, builder)
  97. for (msg in messageList) {
  98. msg.msgBody.richText.ptt?.toAudio()?.let { builder.add(it) }
  99. }
  100. return builder.build().cleanupRubbishMessageElements()
  101. }
  102. /**
  103. * 接收消息的解析器. 将 [MsgComm.Msg] 转换为对应的 [SingleMessage]
  104. * @see joinToMessageChain
  105. */
  106. internal object ReceiveMessageTransformer {
  107. fun createMessageSource(
  108. bot: Bot,
  109. onlineSource: Boolean,
  110. messageSourceKind: MessageSourceKind,
  111. messageList: List<MsgComm.Msg>,
  112. ): MessageSource {
  113. return when (onlineSource) {
  114. true -> {
  115. when (messageSourceKind) {
  116. MessageSourceKind.TEMP -> OnlineMessageSourceFromTempImpl(bot, messageList)
  117. MessageSourceKind.GROUP -> OnlineMessageSourceFromGroupImpl(bot, messageList)
  118. MessageSourceKind.FRIEND -> OnlineMessageSourceFromFriendImpl(bot, messageList)
  119. MessageSourceKind.STRANGER -> OnlineMessageSourceFromStrangerImpl(bot, messageList)
  120. }
  121. }
  122. false -> {
  123. OfflineMessageSourceImplData(bot, messageList, messageSourceKind)
  124. }
  125. }
  126. }
  127. fun joinToMessageChain(
  128. elements: List<ImMsgBody.Elem>,
  129. groupIdOrZero: Long,
  130. messageSourceKind: MessageSourceKind,
  131. bot: Bot,
  132. builder: MessageChainBuilder,
  133. ) {
  134. // ProtoBuf.encodeToHexString(elements).soutv("join")
  135. // (this._miraiContentToString().soutv())
  136. for (element in elements) {
  137. transformElement(element, groupIdOrZero, messageSourceKind, bot, builder)
  138. when {
  139. element.richMsg != null -> decodeRichMessage(element.richMsg, builder)
  140. }
  141. }
  142. }
  143. private fun transformElement(
  144. element: ImMsgBody.Elem,
  145. groupIdOrZero: Long,
  146. messageSourceKind: MessageSourceKind,
  147. bot: Bot,
  148. builder: MessageChainBuilder,
  149. ) {
  150. when {
  151. element.srcMsg != null -> decodeSrcMsg(element.srcMsg, builder, bot, messageSourceKind, groupIdOrZero)
  152. element.notOnlineImage != null -> builder.add(OnlineFriendImageImpl(element.notOnlineImage))
  153. element.customFace != null -> decodeCustomFace(element.customFace, builder)
  154. element.face != null -> builder.add(Face(element.face.index))
  155. element.text != null -> decodeText(element.text, builder)
  156. element.marketFace != null -> builder.add(MarketFaceInternal(element.marketFace))
  157. element.lightApp != null -> decodeLightApp(element.lightApp, builder)
  158. element.customElem != null -> decodeCustomElem(element.customElem, builder)
  159. element.commonElem != null -> decodeCommonElem(element.commonElem, builder)
  160. element.transElemInfo != null -> decodeTransElem(element.transElemInfo, builder)
  161. element.elemFlags2 != null
  162. || element.extraInfo != null
  163. || element.generalFlags != null
  164. || element.anonGroupMsg != null
  165. -> {
  166. // ignore
  167. }
  168. else -> {
  169. UnsupportedMessageImpl(element).takeIf {
  170. it.struct.isNotEmpty()
  171. }?.let(builder::add)
  172. // println(it._miraiContentToString())
  173. }
  174. }
  175. }
  176. fun MessageChainBuilder.compressContinuousPlainText() {
  177. var index = 0
  178. val builder = StringBuilder()
  179. while (index + 1 < size) {
  180. val elm0 = get(index)
  181. val elm1 = get(index + 1)
  182. if (elm0 is PlainText && elm1 is PlainText) {
  183. builder.setLength(0)
  184. var end = -1
  185. for (i in index until size) {
  186. val elm = get(i)
  187. if (elm is PlainText) {
  188. end = i
  189. builder.append(elm.content)
  190. } else break
  191. }
  192. set(index, PlainText(builder.toString()))
  193. // do delete
  194. val index1 = index + 1
  195. repeat(end - index) {
  196. removeAt(index1)
  197. }
  198. }
  199. index++
  200. }
  201. // delete empty plain text
  202. removeAll { it is PlainText && it.content.isEmpty() }
  203. }
  204. fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
  205. val builder = MessageChainBuilder(initialSize = count()).also {
  206. it.addAll(this)
  207. }
  208. kotlin.run moveQuoteReply@{ // Move QuoteReply after MessageSource
  209. val exceptedQuoteReplyIndex = builder.indexOfFirst { it is MessageSource } + 1
  210. val quoteReplyIndex = builder.indexOfFirst { it is QuoteReply }
  211. if (quoteReplyIndex < 1) return@moveQuoteReply
  212. if (quoteReplyIndex != exceptedQuoteReplyIndex) {
  213. val qr = builder[quoteReplyIndex]
  214. builder.removeAt(quoteReplyIndex)
  215. builder.add(exceptedQuoteReplyIndex, qr)
  216. }
  217. }
  218. kotlin.run quote@{
  219. val quoteReplyIndex = builder.indexOfFirst { it is QuoteReply }
  220. if (quoteReplyIndex >= 0) {
  221. // QuoteReply + At + PlainText(space 1)
  222. if (quoteReplyIndex < builder.size - 1) {
  223. if (builder[quoteReplyIndex + 1] is At) {
  224. builder.removeAt(quoteReplyIndex + 1)
  225. }
  226. if (quoteReplyIndex < builder.size - 1) {
  227. val elm = builder[quoteReplyIndex + 1]
  228. if (elm is PlainText && elm.content.startsWith(' ')) {
  229. if (elm.content.length == 1) {
  230. builder.removeAt(quoteReplyIndex + 1)
  231. } else {
  232. builder[quoteReplyIndex + 1] = PlainText(elm.content.substring(1))
  233. }
  234. }
  235. }
  236. return@quote
  237. }
  238. }
  239. }
  240. // TIM audios
  241. if (builder.any { it is Audio }) {
  242. builder.remove(UNSUPPORTED_VOICE_MESSAGE_PLAIN)
  243. }
  244. kotlin.run { // VipFace
  245. val vipFaceIndex = builder.indexOfFirst { it is VipFace }
  246. if (vipFaceIndex >= 0 && vipFaceIndex < builder.size - 1) {
  247. val l = builder[vipFaceIndex] as VipFace
  248. val text = builder[vipFaceIndex + 1]
  249. if (text is PlainText) {
  250. if (text.content.length == 4 + (l.count / 10) + l.kind.name.length) {
  251. builder.removeAt(vipFaceIndex + 1)
  252. }
  253. }
  254. }
  255. }
  256. fun removeSuffixText(index: Int, text: PlainText) {
  257. if (index >= 0 && index < builder.size - 1) {
  258. if (builder[index + 1] == text) {
  259. builder.removeAt(index + 1)
  260. }
  261. }
  262. }
  263. removeSuffixText(builder.indexOfFirst { it is LongMessageInternal }, UNSUPPORTED_MERGED_MESSAGE_PLAIN)
  264. removeSuffixText(builder.indexOfFirst { it is PokeMessage }, UNSUPPORTED_POKE_MESSAGE_PLAIN)
  265. builder.compressContinuousPlainText()
  266. return builder.asMessageChain()
  267. }
  268. private fun decodeText(text: ImMsgBody.Text, list: MessageChainBuilder) {
  269. if (text.attr6Buf.isEmpty()) {
  270. list.add(PlainText(text.str))
  271. } else {
  272. val id: Long
  273. text.attr6Buf.read {
  274. discardExact(7)
  275. id = readUInt().toLong()
  276. }
  277. if (id == 0L) {
  278. list.add(AtAll)
  279. } else {
  280. list.add(At(id)) // element.text.str
  281. }
  282. }
  283. }
  284. private fun decodeSrcMsg(
  285. srcMsg: ImMsgBody.SourceMsg,
  286. list: MessageChainBuilder,
  287. bot: Bot,
  288. messageSourceKind: MessageSourceKind,
  289. groupIdOrZero: Long,
  290. ) {
  291. list.add(QuoteReply(OfflineMessageSourceImplData(srcMsg, bot, messageSourceKind, groupIdOrZero)))
  292. }
  293. private fun decodeCustomFace(
  294. customFace: ImMsgBody.CustomFace,
  295. builder: MessageChainBuilder,
  296. ) {
  297. builder.add(OnlineGroupImageImpl(customFace))
  298. customFace.pbReserve.let {
  299. if (it.isNotEmpty() && it.loadAs(CustomFace.ResvAttr.serializer()).msgImageShow != null) {
  300. builder.add(ShowImageFlag)
  301. }
  302. }
  303. }
  304. private fun decodeLightApp(
  305. lightApp: ImMsgBody.LightAppElem,
  306. list: MessageChainBuilder,
  307. ) {
  308. val content = runWithBugReport("解析 lightApp",
  309. { "resId=" + lightApp.msgResid + "data=" + lightApp.data.toUHexString() }) {
  310. when (lightApp.data[0].toInt()) {
  311. 0 -> lightApp.data.decodeToString(startIndex = 1)
  312. 1 -> lightApp.data.unzip(1).decodeToString()
  313. else -> error("unknown compression flag=${lightApp.data[0]}")
  314. }
  315. }
  316. list.add(LightAppInternal(content))
  317. }
  318. private fun decodeCustomElem(
  319. customElem: ImMsgBody.CustomElem,
  320. list: MessageChainBuilder,
  321. ) {
  322. customElem.data.read {
  323. kotlin.runCatching {
  324. CustomMessage.load(this)
  325. }.fold(
  326. onFailure = {
  327. if (it is CustomMessage.Companion.CustomMessageFullDataDeserializeInternalException) {
  328. throw IllegalStateException(
  329. "Internal error: " +
  330. "exception while deserializing CustomMessage head data," +
  331. " data=${customElem.data.toUHexString()}", it
  332. )
  333. } else {
  334. it as CustomMessage.Companion.CustomMessageFullDataDeserializeUserException
  335. throw IllegalStateException(
  336. "User error: " +
  337. "exception while deserializing CustomMessage body," +
  338. " body=${it.body.toUHexString()}", it
  339. )
  340. }
  341. },
  342. onSuccess = {
  343. if (it != null) {
  344. list.add(it)
  345. }
  346. }
  347. )
  348. }
  349. }
  350. private fun decodeTransElem(
  351. transElement: ImMsgBody.TransElem,
  352. list: MessageChainBuilder,
  353. ) {
  354. // file
  355. // type=24
  356. when (transElement.elemType) {
  357. 24 -> transElement.elemValue.read {
  358. // group file feed
  359. // 01 00 77 08 06 12 0A 61 61 61 61 61 61 2E 74 78 74 1A 06 31 35 42 79 74 65 3A 5F 12 5D 08 66 12 25 2F 64 37 34 62 62 66 33 61 2D 37 62 32 35 2D 31 31 65 62 2D 38 34 66 38 2D 35 34 35 32 30 30 37 62 35 64 39 66 18 0F 22 0A 61 61 61 61 61 61 2E 74 78 74 28 00 3A 00 42 20 61 33 32 35 66 36 33 34 33 30 65 37 61 30 31 31 66 37 64 30 38 37 66 63 33 32 34 37 35 34 39 63
  360. // fun getFileRsrvAttr(file: ObjMsg.MsgContentInfo.MsgFile): HummerResv21.ResvAttr? {
  361. // if (file.ext.isEmpty()) return null
  362. // val element = kotlin.runCatching {
  363. // jsonForFileDecode.parseToJsonElement(file.ext) as? JsonObject
  364. // }.getOrNull() ?: return null
  365. // val extInfo = element["ExtInfo"]?.toString()?.decodeBase64() ?: return null
  366. // return extInfo.loadAs(HummerResv21.ResvAttr.serializer())
  367. // }
  368. val var7 = readByte()
  369. if (var7 == 1.toByte()) {
  370. while (remaining > 2) {
  371. val proto = readProtoBuf(ObjMsg.ObjMsg.serializer(), readUShort().toInt())
  372. // proto.msgType=6
  373. val file = proto.msgContentInfo.firstOrNull()?.msgFile ?: continue // officially get(0) only.
  374. // val attr = getFileRsrvAttr(file) ?: continue
  375. // val info = attr.forwardExtFileInfo ?: continue
  376. list.add(
  377. FileMessageImpl(
  378. id = file.filePath,
  379. busId = file.busId, // path i.e. /a99e95fa-7b2d-11eb-adae-5452007b698a
  380. name = file.fileName,
  381. size = file.fileSize
  382. )
  383. )
  384. }
  385. }
  386. }
  387. }
  388. }
  389. private val jsonForFileDecode = Json {
  390. isLenient = true
  391. coerceInputValues = true
  392. }
  393. private fun decodeCommonElem(
  394. commonElem: ImMsgBody.CommonElem,
  395. list: MessageChainBuilder,
  396. ) {
  397. when (commonElem.serviceType) {
  398. 23 -> {
  399. val proto =
  400. commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype23.serializer())
  401. list.add(VipFace(VipFace.Kind(proto.faceType, proto.faceSummary), proto.faceBubbleCount))
  402. }
  403. 2 -> {
  404. val proto =
  405. commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype2.serializer())
  406. list.add(PokeMessage(
  407. proto.vaspokeName.takeIf { it.isNotEmpty() }
  408. ?: PokeMessage.values.firstOrNull { it.id == proto.vaspokeId && it.pokeType == proto.pokeType }?.name
  409. .orEmpty(),
  410. proto.pokeType,
  411. proto.vaspokeId
  412. )
  413. )
  414. }
  415. 3 -> {
  416. val proto =
  417. commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype3.serializer())
  418. if (proto.flashTroopPic != null) {
  419. list.add(FlashImage(OnlineGroupImageImpl(proto.flashTroopPic)))
  420. }
  421. if (proto.flashC2cPic != null) {
  422. list.add(FlashImage(OnlineFriendImageImpl(proto.flashC2cPic)))
  423. }
  424. }
  425. 33 -> {
  426. val proto =
  427. commonElem.pbElem.loadAs(HummerCommelem.MsgElemInfoServtype33.serializer())
  428. list.add(Face(proto.index))
  429. }
  430. }
  431. }
  432. private fun decodeRichMessage(
  433. richMsg: ImMsgBody.RichMsg,
  434. builder: MessageChainBuilder,
  435. ) {
  436. val content = runWithBugReport("解析 richMsg", { richMsg.template1.toUHexString() }) {
  437. when (richMsg.template1[0].toInt()) {
  438. 0 -> richMsg.template1.decodeToString(startIndex = 1)
  439. 1 -> richMsg.template1.unzip(1).decodeToString()
  440. else -> error("unknown compression flag=${richMsg.template1[0]}")
  441. }
  442. }
  443. fun findStringProperty(name: String): String {
  444. return content.substringAfter("$name=\"", "").substringBefore("\"", "")
  445. }
  446. val serviceId = when (val sid = richMsg.serviceId) {
  447. 0 -> {
  448. val serviceIdStr = findStringProperty("serviceID")
  449. if (serviceIdStr.isEmpty() || serviceIdStr.isBlank()) {
  450. 0
  451. } else {
  452. serviceIdStr.toIntOrNull() ?: 0
  453. }
  454. }
  455. else -> sid
  456. }
  457. when (serviceId) {
  458. // 5: 使用微博长图转换功能分享到QQ群
  459. /*
  460. <?xml version="1.0" encoding="utf-8"?><msg serviceID="5" templateID="12345" brief="[分享]想要沐浴阳光,就别钻进
  461. 阴影。 ???" ><item layout="0"><image uuid="{E5F68BD5-05F8-148B-9DA7-FECD026D30AD}.jpg" md5="E5F68BD505F8148B9DA7FECD026D
  462. 30AD" GroupFiledid="2167263882" minWidth="120" minHeight="120" maxWidth="180" maxHeight="180" /></item><source name="新
  463. 浪微博" icon="http://i.gtimg.cn/open/app_icon/00/73/69/03//100736903_100_m.png" appid="100736903" action="" i_actionData
  464. ="" a_actionData="" url=""/></msg>
  465. */
  466. /**
  467. * json?
  468. */
  469. 1 -> @Suppress("DEPRECATION_ERROR")
  470. builder.add(SimpleServiceMessage(1, content))
  471. /**
  472. * [LongMessageInternal], [ForwardMessage]
  473. */
  474. 35 -> {
  475. val resId = findStringProperty("m_resid")
  476. val fileName = findStringProperty("m_fileName").takeIf { it.isNotEmpty() }
  477. val msg = if (resId.isEmpty()) {
  478. // Nested ForwardMessage
  479. if (fileName != null && findStringProperty("action") == "viewMultiMsg") {
  480. ForwardMessageInternal(content, null, fileName)
  481. } else {
  482. SimpleServiceMessage(35, content)
  483. }
  484. } else when (findStringProperty("multiMsgFlag").toIntOrNull()) {
  485. 1 -> LongMessageInternal(content, resId)
  486. 0 -> ForwardMessageInternal(content, resId, fileName)
  487. else -> {
  488. // from PC QQ
  489. if (findStringProperty("action") == "viewMultiMsg") {
  490. ForwardMessageInternal(content, resId, fileName)
  491. } else {
  492. SimpleServiceMessage(35, content)
  493. }
  494. }
  495. }
  496. builder.add(msg)
  497. }
  498. // 104 新群员入群的消息
  499. else -> {
  500. builder.add(SimpleServiceMessage(serviceId, content))
  501. }
  502. }
  503. }
  504. fun ImMsgBody.Ptt.toAudio() = OnlineAudioImpl(
  505. filename = fileName.decodeToString(),
  506. fileMd5 = fileMd5,
  507. fileSize = fileSize.toLongUnsigned(),
  508. codec = AudioCodec.fromId(format),
  509. url = downPara.decodeToString(),
  510. length = time.toLongUnsigned(),
  511. originalPtt = this,
  512. )
  513. }