ソースを参照

Merge pull request #170 from mamoe/long-message

Support long message in general `sendMessage`
Him188 6 年 前
コミット
f7040c18fb
43 ファイル変更281 行追加294 行削除
  1. 2 2
      mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt
  2. 20 32
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
  3. 14 4
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt
  4. 2 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/MemberImpl.kt
  5. 22 6
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt
  6. 3 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt
  7. 27 149
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt
  8. 10 27
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
  9. 12 20
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt
  10. 28 18
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
  11. 5 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt
  12. 2 2
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/TEA.kt
  13. 8 10
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt
  14. 2 2
      mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt
  15. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt
  16. 4 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaFriendlyAPI.kt
  17. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Contact.kt
  18. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/ContactJavaFriendlyAPI.kt
  19. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Group.kt
  20. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Member.kt
  21. 2 0
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/QQ.kt
  22. 7 2
      mirai-core/src/androidMain/kotlin/net/mamoe/mirai/message/MessageReceipt.kt
  23. 11 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt
  24. 2 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt
  25. 2 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
  26. 2 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt
  27. 2 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/QQ.kt
  28. 3 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt
  29. 2 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt
  30. 15 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessageReceipt.kt
  31. 3 3
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt
  32. 13 3
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt
  33. 20 2
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt
  34. 4 1
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt
  35. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt
  36. 4 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaFriendlyAPI.kt
  37. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Contact.kt
  38. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJavaFriendlyAPI.kt
  39. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt
  40. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Member.kt
  41. 2 0
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/QQ.kt
  42. 5 1
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/MessageReceipt.kt
  43. 1 1
      mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt

+ 2 - 2
mirai-core-qqandroid/src/androidMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHAndroid.kt

@@ -32,9 +32,9 @@ internal actual class ECDHKeyPairImpl(
 }
 
 @Suppress("FunctionName")
-actual fun ECDH() = ECDH(ECDH.generateKeyPair())
+internal actual fun ECDH() = ECDH(ECDH.generateKeyPair())
 
-actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
+internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
     actual companion object {
         @Suppress("ObjectPropertyName")
         private var _isECDHAvailable: Boolean = false // because `runCatching` has no contract.

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

@@ -7,6 +7,8 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("EXPERIMENTAL_API_USAGE")
+
 package net.mamoe.mirai.qqandroid
 
 import io.ktor.client.HttpClient
@@ -29,12 +31,12 @@ import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.*
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.MessageRecallEvent
+import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.contact.MemberInfoImpl
 import net.mamoe.mirai.qqandroid.contact.QQImpl
 import net.mamoe.mirai.qqandroid.contact.checkIsGroupImpl
 import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
-import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
 import net.mamoe.mirai.qqandroid.message.OnlineFriendImageImpl
 import net.mamoe.mirai.qqandroid.message.OnlineGroupImageImpl
 import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
@@ -162,6 +164,7 @@ internal abstract class QQAndroidBotBase constructor(
         TODO("not implemented")
     }
 
+    @ExperimentalMessageSource
     override suspend fun recall(source: MessageSource) {
         if (source.senderId != uin && source.groupId != 0L) {
             getGroup(source.groupId).checkBotPermissionOperator()
@@ -368,34 +371,21 @@ internal abstract class QQAndroidBotBase constructor(
 
     @LowLevelAPI
     @MiraiExperimentalAPI
-    override suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message) {
+    override suspend fun _lowLevelSendLongGroupMessage(groupCode: Long, message: Message): MessageReceipt<Group> {
         val chain = message.asMessageChain()
-        check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "message is too large" }
+        check(chain.toString().length <= 4500 && chain.count { it is Image } <= 50) { "message is too large. Allow up to 4500 chars or 50 images" }
         val group = getGroup(groupCode)
 
-        val source = MessageSourceFromSendFriend(
-            messageRandom = Random.nextInt().absoluteValue,
-            senderId = client.uin,
-            toUin = Group.calculateGroupUinByGroupCode(groupCode),
-            time = currentTimeSeconds,
-            groupId = groupCode,
-            originalMessage = chain,
-            sequenceId = client.atomicNextMessageSequenceId()
-            //   sourceMessage = message
-        )
-
-        // TODO: 2020/3/26 util 方法来添加单例元素
-        val toSend = buildMessageChain(chain.size) {
-            source.originalMessage.forEach {
-                if (it !is MessageSource) {
-                    add(it)
-                }
-            }
-            add(source)
-        }
+        val time = currentTimeSeconds
 
         network.run {
-            val data = toSend.calculateValidationDataForGroup(group)
+            val data = chain.calculateValidationDataForGroup(
+                sequenceId = client.atomicNextMessageSequenceId(),
+                time = time.toInt(),
+                random = Random.nextInt().absoluteValue.toUInt(),
+                groupCode,
+                group.botAsMember.nameCardOrNick
+            )
 
             val response =
                 MultiMsg.ApplyUp.createForGroupLongMessage(
@@ -441,17 +431,15 @@ internal abstract class QQAndroidBotBase constructor(
                 }
             }
 
-            group.sendMessage(
+            return group.sendMessage(
                 RichMessage.longMessage(
-                    brief = toSend.joinToString(limit = 30) {
-                        when (it) {
-                            is PlainText -> it.stringValue
-                            is At -> it.toString()
-                            else -> ""
-                        }
+                    brief = chain.toString().let { // already cached
+                        if (it.length > 27) {
+                            it.take(27) + "..."
+                        } else it
                     },
                     resId = resId,
-                    timeSeconds = source.time
+                    timeSeconds = time
                 )
             )
         }

+ 14 - 4
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/GroupImpl.kt

@@ -23,9 +23,7 @@ import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.event.events.MessageSendEvent.GroupMessageSendEvent
 import net.mamoe.mirai.message.MessageReceipt
-import net.mamoe.mirai.message.data.Message
-import net.mamoe.mirai.message.data.OfflineGroupImage
-import net.mamoe.mirai.message.data.asMessageChain
+import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
 import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
@@ -273,13 +271,25 @@ internal class GroupImpl(
         return members.delegate.filteringGetOrNull { it.id == id }
     }
 
+    @OptIn(MiraiExperimentalAPI::class)
     @JvmSynthetic
     override suspend fun sendMessage(message: Message): MessageReceipt<Group> {
         check(!isBotMuted) { "bot is muted. Remaining seconds=$botMuteRemaining" }
         val event = GroupMessageSendEvent(this, message.asMessageChain()).broadcast()
         if (event.isCancelled) {
-            throw EventCancelledException("cancelled by FriendMessageSendEvent")
+            throw EventCancelledException("cancelled by GroupMessageSendEvent")
+        }
+
+        if (message !is LongMessage) {
+            val length = event.message.toString().length
+            if (length > 4000
+                || event.message.count { it is Image } > 3
+                || (event.message.any<QuoteReply>() && (event.message.any<Image>() || length > 100))
+            ) {
+                return bot._lowLevelSendLongGroupMessage(this.id, message)
+            }
         }
+
         lateinit var source: MessageSourceFromSendGroup
         bot.network.run {
             val response: MessageSvc.PbSendMsg.Response = MessageSvc.PbSendMsg.ToGroup(

+ 2 - 0
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/MemberImpl.kt

@@ -7,6 +7,8 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("EXPERIMENTAL_API_USAGE")
+
 package net.mamoe.mirai.qqandroid.contact
 
 import kotlinx.coroutines.launch

+ 22 - 6
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/contact/QQImpl.kt

@@ -8,9 +8,12 @@
  */
 
 @file:OptIn(MiraiInternalAPI::class, LowLevelAPI::class)
+@file:Suppress("EXPERIMENTAL_API_USAGE")
 
 package net.mamoe.mirai.qqandroid.contact
 
+import kotlinx.atomicfu.AtomicInt
+import kotlinx.atomicfu.atomic
 import kotlinx.io.core.Closeable
 import net.mamoe.mirai.LowLevelAPI
 import net.mamoe.mirai.contact.Contact
@@ -36,6 +39,8 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.LongConn
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.toUHexString
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
 import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmSynthetic
 
@@ -46,12 +51,23 @@ internal inline class FriendInfoImpl(
     override val uin: Long get() = jceFriendInfo.friendUin
 }
 
+@OptIn(ExperimentalContracts::class)
+internal fun QQ.checkIsQQImpl(): QQImpl {
+    contract {
+        returns() implies (this@checkIsQQImpl is QQImpl)
+    }
+    check(this is QQImpl) { "A QQ instance is not instance of QQImpl. Don't interlace two protocol implementations together!" }
+    return this
+}
+
 internal class QQImpl(
     bot: QQAndroidBot,
     override val coroutineContext: CoroutineContext,
     override val id: Long,
     private val friendInfo: FriendInfo
 ) : QQ() {
+    var lastMessageSequence: AtomicInt = atomic(-1)
+
     override val bot: QQAndroidBot by bot.unsafeWeakRef()
     override val nick: String
         get() = friendInfo.nick
@@ -67,12 +83,12 @@ internal class QQImpl(
         bot.network.run {
             check(
                 MessageSvc.PbSendMsg.ToFriend(
-                    bot.client,
-                    id,
-                    event.message
-                ) {
-                    source = it
-                }
+                        bot.client,
+                        id,
+                        event.message
+                    ) {
+                        source = it
+                    }
                     .sendAndExpect<MessageSvc.PbSendMsg.Response>() is MessageSvc.PbSendMsg.Response.SUCCESS
             ) { "send message failed" }
         }

+ 3 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/MessageSourceImpl.kt

@@ -7,6 +7,8 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
+@file:Suppress("EXPERIMENTAL_API_USAGE", "EXPERIMENTAL_OVERRIDE")
+
 package net.mamoe.mirai.qqandroid.message
 
 import kotlinx.coroutines.CoroutineScope
@@ -146,7 +148,7 @@ internal abstract class MessageSourceFromSend : MessageSource {
     }
 
     private val elems by lazy {
-        originalMessage.toRichTextElems(groupId != 0L)
+        originalMessage.toRichTextElems(groupId != 0L, true)
     }
 
     private fun toJceDataImplForFriend(): ImMsgBody.SourceMsg {

+ 27 - 149
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/message/messages.kt

@@ -7,6 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 @file: OptIn(MiraiExperimentalAPI::class, MiraiInternalAPI::class, LowLevelAPI::class, ExperimentalUnsignedTypes::class)
+@file:Suppress("EXPERIMENTAL_API_USAGE")
 
 package net.mamoe.mirai.qqandroid.message
 
@@ -56,44 +57,6 @@ internal fun OfflineFriendImage.toJceData(): ImMsgBody.NotOnlineImage {
     )
 }
 
-/*
-CustomFace#24412994 {
-guid=<Empty ByteArray>
-filePath={01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
-shortcut=
-buffer=<Empty ByteArray>
-flag=00 00 00 00
-oldData=15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41
-fileId=0x872F0660(-2026961312)
-serverIp=0x3AE103B7(987825079)
-serverPort=0x00000050(80)
-fileType=0x00000000(0)
-signature=<Empty ByteArray>
-useful=0x00000001(1)
-md5=01 E9 45 1B 70 ED EA E3 B3 7C 10 1F 1E EB F5 B5
-thumbUrl=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/198?term=2
-bigUrl=
-origUrl=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/0?term=2
-bizType=0x00000000(0)
-repeatIndex=0x00000000(0)
-repeatImage=0x00000000(0)
-imageType=0x00000000(0)
-index=0x00000000(0)
-width=0x0000015F(351)
-height=0x000000EB(235)
-source=0x00000000(0)
-size=0x0000057C(1404)
-origin=0x00000000(0)
-thumbWidth=0x000000C6(198)
-thumbHeight=0x00000084(132)
-showLen=0x00000000(0)
-downloadLen=0x00000000(0)
-_400Url=/gchatpic_new/1040400290/1041235568-2268005984-01E9451B70EDEAE3B37C101F1EEBF5B5/400?term=2
-_400Width=0x0000015F(351)
-_400Height=0x000000EB(235)
-pbReserve=<Empty ByteArray>
-}
- */
 internal val FACE_BUF = "00 01 00 04 52 CC F5 D0".hexToBytes()
 
 internal fun Face.toJceData(): ImMsgBody.Face {
@@ -133,76 +96,6 @@ internal fun OfflineGroupImage.toJceData(): ImMsgBody.CustomFace {
 private val oldData: ByteArray =
     "15 36 20 39 32 6B 41 31 00 38 37 32 66 30 36 36 30 33 61 65 31 30 33 62 37 20 20 20 20 20 20 35 30 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 7B 30 31 45 39 34 35 31 42 2D 37 30 45 44 2D 45 41 45 33 2D 42 33 37 43 2D 31 30 31 46 31 45 45 42 46 35 42 35 7D 2E 70 6E 67 41".hexToBytes()
 
-/*
-customFace=CustomFace#2050019814 {
-        guid=<Empty ByteArray>
-        filePath=5F6C522DEAC4F36C0ED8EF362660EFD6.png
-        shortcut=
-        buffer=<Empty ByteArray>
-        flag=<Empty ByteArray>
-        oldData=<Empty ByteArray>
-        fileId=0xB40AF10E(-1274351346)
-        serverIp=0xB703E13A(-1224482502)
-        serverPort=0x00000050(80)
-        fileType=0x00000042(66)
-        signature=6B 44 61 76 72 79 68 79 57 67 70 52 41 45 78 49
-        useful=0x00000001(1)
-        md5=5F 6C 52 2D EA C4 F3 6C 0E D8 EF 36 26 60 EF D6
-        thumbUrl=
-        bigUrl=
-        origUrl=
-        bizType=0x00000005(5)
-        repeatIndex=0x00000000(0)
-        repeatImage=0x00000000(0)
-        imageType=0x000003E9(1001)
-        index=0x00000000(0)
-        width=0x0000005F(95)
-        height=0x00000054(84)
-        source=0x00000067(103)
-        size=0x000006E2(1762)
-        origin=0x00000000(0)
-        thumbWidth=0x00000000(0)
-        thumbHeight=0x00000000(0)
-        showLen=0x00000000(0)
-        downloadLen=0x00000000(0)
-        _400Url=
-        _400Width=0x00000000(0)
-        _400Height=0x00000000(0)
-        pbReserve=08 01 10 00 32 00 4A 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
-}
-
-notOnlineImage=NotOnlineImage#2050019814 {
-        filePath=41AEF2D4B5BD24CF3791EFC5FEB67D60.jpg
-        fileLen=0x00000350(848)
-        downloadPath=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
-        oldVerSendFile=<Empty ByteArray>
-        imgType=0x000003E8(1000)
-        previewsImage=<Empty ByteArray>
-        picMd5=41 AE F2 D4 B5 BD 24 CF 37 91 EF C5 FE B6 7D 60
-        picHeight=0x00000032(50)
-        picWidth=0x00000033(51)
-        resId=/f2b7e5c0-acb3-4e83-aa5c-c8383840cc91
-        flag=<Empty ByteArray>
-        thumbUrl=
-        original=0x00000000(0)
-        bigUrl=
-        origUrl=
-        bizType=0x00000005(5)
-        result=0x00000000(0)
-        index=0x00000000(0)
-        opFaceBuf=<Empty ByteArray>
-        oldPicMd5=false
-        thumbWidth=0x00000000(0)
-        thumbHeight=0x00000000(0)
-        fileId=0x00000000(0)
-        showLen=0x00000000(0)
-        downloadLen=0x00000000(0)
-        _400Url=
-        _400Width=0x00000000(0)
-        _400Height=0x00000000(0)
-        pbReserve=08 01 10 00 32 00 42 0E 5B E5 8A A8 E7 94 BB E8 A1 A8 E6 83 85 5D 50 00 78 05
-}
- */
 
 private val atAllData = ImMsgBody.Elem(
     text = ImMsgBody.Text(
@@ -222,7 +115,7 @@ private val atAllData = ImMsgBody.Elem(
 private val UNSUPPORTED_MERGED_MESSAGE_PLAIN = PlainText("你的QQ暂不支持查看[转发多条消息],请期待后续版本。")
 
 @OptIn(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
-internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgBody.Elem> {
+internal fun MessageChain.toRichTextElems(forGroup: Boolean, withGeneralFlags: Boolean): MutableList<ImMsgBody.Elem> {
     val elements = mutableListOf<ImMsgBody.Elem>()
 
     if (this.any<QuoteReply>()) {
@@ -310,28 +203,34 @@ internal fun MessageChain.toRichTextElems(forGroup: Boolean): MutableList<ImMsgB
     }
     this.forEach(::transformOneMessage)
 
-    when {
-        longTextResId != null -> {
-            elements.add(
-                ImMsgBody.Elem(
-                    generalFlags = ImMsgBody.GeneralFlags(
-                        longTextFlag = 1,
-                        longTextResid = longTextResId!!,
-                        pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
-                    ),
+    if (withGeneralFlags) {
+        when {
+            longTextResId != null -> {
+                elements.add(
+                    ImMsgBody.Elem(
+                        generalFlags = ImMsgBody.GeneralFlags(
+                            longTextFlag = 1,
+                            longTextResid = longTextResId!!,
+                            pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes()
+                        ),
+                    )
                 )
-            )
-        }
-        this.any<RichMessage>() -> {
-            // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
-            elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes())))
+            }
+            this.any<RichMessage>() -> {
+                // 08 09 78 00 A0 01 81 DC 01 C8 01 00 F0 01 00 F8 01 00 90 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00
+                elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_RICH_MESSAGE)))
+            }
+            else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = PB_RESERVE_FOR_ELSE)))
         }
-        else -> elements.add(ImMsgBody.Elem(generalFlags = ImMsgBody.GeneralFlags(pbReserve = "78 00 F8 01 00 C8 02 00".hexToBytes())))
     }
 
     return elements
 }
 
+private val PB_RESERVE_FOR_RICH_MESSAGE =
+    "08 09 78 00 C8 01 00 F0 01 00 F8 01 00 90 02 00 C8 02 00 98 03 00 A0 03 20 B0 03 00 C0 03 00 D0 03 00 E8 03 00 8A 04 02 08 03 90 04 80 80 80 10 B8 04 00 C0 04 00".hexToBytes()
+private val PB_RESERVE_FOR_ELSE = "78 00 F8 01 00 C8 02 00".hexToBytes()
+
 internal class OnlineGroupImageImpl(
     internal val delegate: ImMsgBody.CustomFace
 ) : OnlineGroupImage() {
@@ -416,15 +315,10 @@ private fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
     var last: SingleMessage? = null
     return buildMessageChain(initialSize = this.count()) {
         [email protected] { element ->
-            if (last == null) {
-                last = element
-                return@forEach
-            } else {
-                if (last is LongMessage && element is PlainText) {
-                    if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
-                        last = element
-                        return@forEach
-                    }
+            if (last is LongMessage && element is PlainText) {
+                if (element == UNSUPPORTED_MERGED_MESSAGE_PLAIN) {
+                    last = element
+                    return@forEach
                 }
             }
 
@@ -443,19 +337,6 @@ internal inline fun <reified R> Iterable<*>.firstIsInstance(): R {
     throw NoSuchElementException("Collection contains no element matching the predicate.")
 }
 
-/*
-    if (this.any<QuoteReply>()) {
-        var removed = false
-        this.filter {
-            if (it is At && !removed) {
-                false
-            } else {
-                removed = true
-                true
-            }
-        }.asMessageChain()
-    } else this*/
-
 internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilder) {
     this.forEach {
         when {
@@ -467,9 +348,6 @@ internal fun List<ImMsgBody.Elem>.joinToMessageChain(message: MessageChainBuilde
                 if (it.text.attr6Buf.isEmpty()) {
                     message.add(it.text.str.toMessage())
                 } else {
-                    // 00 01 00 00 00 05 01 00 00 00 00 00 00 all
-                    // 00 01 00 00 00 0A 00 3E 03 3F A2 00 00 one/nick
-                    // 00 01 00 00 00 07 00 44 71 47 90 00 00 one/groupCard
                     val id: Long
                     it.text.attr6Buf.read {
                         discardExact(7)

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

@@ -27,14 +27,17 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.StatSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.WtLogin
 import net.mamoe.mirai.qqandroid.network.readUShortLVByteArray
+import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
+import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
 import net.mamoe.mirai.qqandroid.utils.io.readPacketExact
 import net.mamoe.mirai.qqandroid.utils.io.readString
 import net.mamoe.mirai.qqandroid.utils.io.useBytes
 import net.mamoe.mirai.qqandroid.utils.io.withUse
 import net.mamoe.mirai.utils.*
-import net.mamoe.mirai.qqandroid.utils.cryptor.TEA
-import net.mamoe.mirai.qqandroid.utils.cryptor.adjustToPublicKey
-import net.mamoe.mirai.utils.io.*
+import net.mamoe.mirai.utils.io.ByteArrayPool
+import net.mamoe.mirai.utils.io.toInt
+import net.mamoe.mirai.utils.io.toReadPacket
+import net.mamoe.mirai.utils.io.toUHexString
 import kotlin.jvm.JvmName
 
 
@@ -171,9 +174,6 @@ internal object KnownPacketFactories {
             ?: IncomingFactories.firstOrNull { it.receivingCommandName == commandName }
     }
 
-    /**
-     * full packet without length
-     */
     // do not inline. Exceptions thrown will not be reported correctly
     @OptIn(MiraiInternalAPI::class)
     @Suppress("UNCHECKED_CAST")
@@ -183,22 +183,12 @@ internal object KnownPacketFactories {
             val flag1 = readInt()
 
             PacketLogger.verbose { "开始处理一个包" }
-            PacketLogger.verbose { "flag1(0A/0B) = ${flag1.toUByte().toUHexString()}" }
 
             val flag2 = readByte().toInt()
-            PacketLogger.verbose {
-                "包类型(flag2) = $flag2. (可能是 ${when (flag2) {
-                    2 -> "OicqRequest"
-                    1 -> "Uni/ProtoBuf"
-                    0 -> "Heartbeat"
-                    else -> "未知"
-                }})"
-            }
-
             val flag3 = readByte().toInt()
             check(flag3 == 0) {
-                "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. Remaining=${this.readBytes()
-                    .toUHexString()}"
+                "Illegal flag3. Expected 0, whereas got $flag3. flag1=$flag1, flag2=$flag2. " +
+                        "Remaining=${this.readBytes().toUHexString()}"
             }
 
             readString(readInt() - 4)// uinAccount
@@ -209,14 +199,11 @@ internal object KnownPacketFactories {
                 kotlin.runCatching {
                     when (flag2) {
                         2 -> TEA.decrypt(data, DECRYPTER_16_ZERO, size)
-                            .also { PacketLogger.verbose { "成功使用 16 zero 解密" } }
                         1 -> TEA.decrypt(data, bot.client.wLoginSigInfo.d2Key, size)
-                            .also { PacketLogger.verbose { "成功使用 d2Key 解密" } }
                         0 -> data
                         else -> error("")
                     }
                 }.getOrElse {
-                    PacketLogger.verbose { "失败, 尝试其他各种key" }
                     bot.client.tryDecryptOrNull(data, size) { it }
                 }?.toReadPacket()?.let { decryptedData ->
                     when (flag1) {
@@ -236,7 +223,7 @@ internal object KnownPacketFactories {
                     } else {
                         handleIncomingPacket(it, bot, flag2, consumer)
                     }
-                } ?: inline {
+                } ?: kotlin.run {
                     PacketLogger.error { "任何key都无法解密: ${data.take(size).toUHexString()}" }
                     return
                 }
@@ -295,8 +282,6 @@ internal object KnownPacketFactories {
         }
     }
 
-    private inline fun <R> inline(block: () -> R): R = block()
-
     class IncomingPacket<T : Packet?>(
         val packetFactory: PacketFactory<T>?,
         val sequenceId: Int,
@@ -358,9 +343,7 @@ internal object KnownPacketFactories {
                     }
                 }
             }
-            8 -> {
-                input
-            }
+            8 -> input
             else -> error("unknown dataCompressed flag: $dataCompressed")
         }
 

ファイルの差分が大きいため隠しています
+ 12 - 20
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/MultiMsg.kt


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

@@ -11,6 +11,7 @@
 
 package net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive
 
+import kotlinx.atomicfu.loop
 import kotlinx.coroutines.FlowPreview
 import kotlinx.coroutines.flow.*
 import kotlinx.io.core.ByteReadPacket
@@ -19,16 +20,15 @@ import net.mamoe.mirai.LowLevelAPI
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.data.MemberInfo
-import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
-import net.mamoe.mirai.qqandroid.network.Packet
 import net.mamoe.mirai.event.events.BotJoinGroupEvent
 import net.mamoe.mirai.event.events.BotOfflineEvent
 import net.mamoe.mirai.event.events.MemberJoinEvent
 import net.mamoe.mirai.getFriendOrNull
 import net.mamoe.mirai.message.FriendMessage
 import net.mamoe.mirai.message.data.MessageChain
-import net.mamoe.mirai.qqandroid.contact.GroupImpl
 import net.mamoe.mirai.qqandroid.QQAndroidBot
+import net.mamoe.mirai.qqandroid.contact.GroupImpl
+import net.mamoe.mirai.qqandroid.contact.checkIsQQImpl
 import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
 import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
 import net.mamoe.mirai.qqandroid.io.serialization.toByteArray
@@ -37,6 +37,8 @@ import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendFriend
 import net.mamoe.mirai.qqandroid.message.MessageSourceFromSendGroup
 import net.mamoe.mirai.qqandroid.message.toMessageChain
 import net.mamoe.mirai.qqandroid.message.toRichTextElems
+import net.mamoe.mirai.qqandroid.network.MultiPacketByIterable
+import net.mamoe.mirai.qqandroid.network.Packet
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushForceOffline
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPushNotify
@@ -195,9 +197,8 @@ internal class MessageSvc {
                                 bot.groups.delegate.addLast(newGroup)
                                 return@mapNotNull BotJoinGroupEvent(newGroup)
                             } else {
-                                if (group == null) {
-                                    return@mapNotNull null
-                                }
+                                group ?: return@mapNotNull null
+
                                 if (group.members.contains(msg.msgHead.authUin)) {
                                     return@mapNotNull null
                                 }
@@ -207,21 +208,30 @@ internal class MessageSvc {
                                     override val specialTitle: String get() = ""
                                     override val muteTimestamp: Int get() = 0
                                     override val uin: Long get() = msg.msgHead.authUin
-                                    override val nick: String
-                                        get() = msg.msgHead.authNick.takeIf { it.isNotEmpty() }
-                                            ?: msg.msgHead.fromNick
+                                    override val nick: String = msg.msgHead.authNick.takeIf { it.isNotEmpty() }
+                                        ?: msg.msgHead.fromNick
                                 }).also { group.members.delegate.addLast(it) })
                             }
                         }
                         166 -> {
                             val friend = bot.getFriendOrNull(msg.msgHead.fromUin) ?: return@mapNotNull null
-                            return@mapNotNull when {
-                                msg.msgHead.fromUin == bot.uin -> null
-                                !bot.firstLoginSucceed -> null
-                                else -> FriendMessage(
-                                    friend,
-                                    msg.toMessageChain()
-                                )
+                            friend.checkIsQQImpl()
+
+                            if (msg.msgHead.fromUin == bot.uin || !bot.firstLoginSucceed) {
+                                return@mapNotNull null
+                            }
+
+                            friend.lastMessageSequence.loop { instant ->
+                                if (msg.msgHead.msgSeq > instant) {
+                                    println("bigger")
+                                    if (friend.lastMessageSequence.compareAndSet(instant, msg.msgHead.msgSeq)) {
+                                        println("set ok")
+                                        return@mapNotNull FriendMessage(
+                                            friend,
+                                            msg.toMessageChain()
+                                        )
+                                    }
+                                } else return@mapNotNull null
                             }
                         }
                         else -> return@mapNotNull null
@@ -319,7 +329,7 @@ internal class MessageSvc {
                     contentHead = MsgComm.ContentHead(pkgNum = 1),
                     msgBody = ImMsgBody.MsgBody(
                         richText = ImMsgBody.RichText(
-                            elems = message.toRichTextElems(false)
+                            elems = message.toRichTextElems(false, true)
                         )
                     ),
                     msgSeq = source.sequenceId,
@@ -372,7 +382,7 @@ internal class MessageSvc {
                     contentHead = MsgComm.ContentHead(pkgNum = 1),
                     msgBody = ImMsgBody.MsgBody(
                         richText = ImMsgBody.RichText(
-                            elems = message.toRichTextElems(true)
+                            elems = message.toRichTextElems(true, true)
                         )
                     ),
                     msgSeq = client.atomicNextMessageSequenceId(),

+ 5 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDH.kt

@@ -21,7 +21,7 @@ expect interface ECDHPublicKey {
 
 internal expect class ECDHKeyPairImpl : ECDHKeyPair
 
-interface ECDHKeyPair {
+internal interface ECDHKeyPair {
     val privateKey: ECDHPrivateKey
     val publicKey: ECDHPublicKey
 
@@ -43,7 +43,7 @@ interface ECDHKeyPair {
 /**
  * 椭圆曲线密码, ECDH 加密
  */
-expect class ECDH(keyPair: ECDHKeyPair) {
+internal expect class ECDH(keyPair: ECDHKeyPair) {
     val keyPair: ECDHKeyPair
 
     /**
@@ -74,9 +74,9 @@ expect class ECDH(keyPair: ECDHKeyPair) {
 }
 
 @Suppress("FunctionName")
-expect fun ECDH(): ECDH
+internal expect fun ECDH(): ECDH
 
-val initialPublicKey
+internal val initialPublicKey
     get() = ECDH.constructPublicKey("3046301006072A8648CE3D020106052B8104001F03320004928D8850673088B343264E0C6BACB8496D697799F37211DEB25BB73906CB089FEA9639B4E0260498B51A992D50813DA8".chunkedHexToBytes())
 private val commonHeadFor02 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
 private val commonHeadForNot02 = "3046301006072A8648CE3D020106052B8104001F033200".chunkedHexToBytes()
@@ -86,7 +86,7 @@ private val byteArray_04 = byteArrayOf(0x04)
 
 private val head1 = "302E301006072A8648CE3D020106052B8104001F031A00".chunkedHexToBytes()
 private val head2 = "3046301006072A8648CE3D020106052B8104001F03320004".chunkedHexToBytes()
-fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
+internal fun ByteArray.adjustToPublicKey(): ECDHPublicKey {
     val head = if (this.size < 30) head1 else head2
 
     return ECDH.constructPublicKey(head + this)

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

@@ -23,7 +23,7 @@ import kotlin.random.Random
 /**
  * 解密错误
  */
-class DecryptionFailedException : Exception {
+internal class DecryptionFailedException : Exception {
     constructor() : super()
     constructor(message: String?) : super(message)
 }
@@ -34,7 +34,7 @@ class DecryptionFailedException : Exception {
  * **注意**: 此为 Mirai 内部 API. 它可能会在任何时刻被改变.
  */
 @MiraiInternalAPI
-object TEA {
+internal object TEA {
     // TODO: 2020/2/28 使用 stream 式输入以避免缓存
 
     /**

+ 8 - 10
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/utils/io/output.kt

@@ -20,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 
-fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLength: Int) {
+internal fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLength: Int) {
     if (array.size <= maxLength) {
         writeShort(array.size.toShort())
         writeFully(array)
@@ -32,13 +32,13 @@ fun BytePacketBuilder.writeShortLVByteArrayLimitedLength(array: ByteArray, maxLe
     }
 }
 
-inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int {
+internal inline fun BytePacketBuilder.writeShortLVByteArray(byteArray: ByteArray): Int {
     this.writeShort(byteArray.size.toShort())
     this.writeFully(byteArray)
     return byteArray.size
 }
 
-inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
+internal inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
     BytePacketBuilder().apply(builder).build().use {
         if (tag != null) writeUByte(tag)
         val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
@@ -47,7 +47,7 @@ inline fun BytePacketBuilder.writeIntLVPacket(tag: UByte? = null, lengthOffset:
         return length.toInt()
     }
 
-inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
+internal inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset: ((Long) -> Long) = {it}, builder: BytePacketBuilder.() -> Unit): Int =
     BytePacketBuilder().apply(builder).build().use {
         if (tag != null) writeUByte(tag)
         val length = lengthOffset.invoke(it.remaining).coerceAtMostOrFail(0xFFFFFFFFL)
@@ -56,18 +56,16 @@ inline fun BytePacketBuilder.writeShortLVPacket(tag: UByte? = null, lengthOffset
         return length.toInt()
     }
 
-inline fun BytePacketBuilder.writeShortLVString(str: String) = writeShortLVByteArray(str.toByteArray())
+internal inline fun BytePacketBuilder.writeShortLVString(str: String) = writeShortLVByteArray(str.toByteArray())
 
-fun BytePacketBuilder.writeHex(uHex: String) {
+internal fun BytePacketBuilder.writeHex(uHex: String) {
     uHex.split(" ").forEach {
         if (it.isNotBlank()) {
             writeUByte(it.toUByte(16))
         }
     }
 }
-/**
- * 会使用 [ByteArrayPool] 缓存
- */
+
 @OptIn(MiraiInternalAPI::class)
-inline fun BytePacketBuilder.encryptAndWrite(key: ByteArray, encoder: BytePacketBuilder.() -> Unit) =
+internal inline fun BytePacketBuilder.encryptAndWrite(key: ByteArray, encoder: BytePacketBuilder.() -> Unit) =
     TEA.encrypt(BytePacketBuilder().apply(encoder).build(), key) { decrypted -> writeFully(decrypted) }

+ 2 - 2
mirai-core-qqandroid/src/jvmMain/kotlin/net/mamoe/mirai/qqandroid/utils/cryptor/ECDHJvm.kt

@@ -32,10 +32,10 @@ internal actual class ECDHKeyPairImpl(
 }
 
 @Suppress("FunctionName")
-actual fun ECDH() =
+internal actual fun ECDH() =
     ECDH(ECDH.generateKeyPair())
 
-actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
+internal actual class ECDH actual constructor(actual val keyPair: ECDHKeyPair) {
     actual companion object {
         actual val isECDHAvailable: Boolean
 

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/Bot.kt

@@ -7,6 +7,7 @@ import kotlinx.coroutines.io.ByteReadChannel
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.AddFriendResult
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.ExperimentalMessageSource
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSource
@@ -152,6 +153,7 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
      * @see _lowLevelRecallFriendMessage 低级 API
      * @see _lowLevelRecallGroupMessage 低级 API
      */
+    @ExperimentalMessageSource
     @JvmSynthetic
     actual abstract suspend fun recall(source: MessageSource)
 

+ 4 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/BotJavaFriendlyAPI.kt

@@ -5,6 +5,7 @@ import net.mamoe.mirai.contact.PermissionDeniedException
 import net.mamoe.mirai.contact.recall
 import net.mamoe.mirai.data.AddFriendResult
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.ExperimentalMessageSource
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSource
@@ -61,6 +62,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
      *
      * @see Bot.recall (扩展函数) 接受参数 [MessageChain]
      */
+    @ExperimentalMessageSource
     @JvmName("recall")
     fun __recallBlockingForJava__(source: MessageSource) {
         runBlocking { recall(source) }
@@ -88,6 +90,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
      * @param millis 延迟的时间, 单位为毫秒
      * @see recall
      */
+    @ExperimentalMessageSource
     @JvmName("recallIn")
     fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) {
         runBlocking { recallIn(source, millis) }
@@ -148,6 +151,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
     /**
      * 异步调用 [__recallBlockingForJava__]
      */
+    @ExperimentalMessageSource
     @JvmName("recallAsync")
     fun __recallAsyncForJava__(source: MessageSource): Future<Unit> {
         return future { recall(source) }

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Contact.kt

@@ -54,6 +54,8 @@ actual abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI() {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/ContactJavaFriendlyAPI.kt

@@ -48,6 +48,8 @@ actual abstract class ContactJavaFriendlyAPI {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Group.kt

@@ -125,6 +125,8 @@ actual abstract class Group : Contact(), CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/Member.kt

@@ -110,6 +110,8 @@ actual abstract class Member : MemberJavaFriendlyAPI() {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/contact/QQ.kt

@@ -78,6 +78,8 @@ actual abstract class QQ : Contact(), CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 7 - 2
mirai-core/src/androidMain/kotlin/net/mamoe/mirai/message/MessageReceipt.kt

@@ -29,7 +29,8 @@ import net.mamoe.mirai.utils.unsafeWeakRef
  */
 @Suppress("FunctionName")
 @OptIn(MiraiInternalAPI::class)
-actual open class MessageReceipt<C : Contact> actual constructor(
+actual open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class)
+actual constructor(
     actual val source: MessageSource,
     target: C,
     private val botAsMember: Member?
@@ -56,6 +57,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      * @see Bot.recall
      * @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
      */
+    @OptIn(ExperimentalMessageSource::class)
     actual suspend fun recall() {
         @Suppress("BooleanLiteralArgument")
         if (_isRecalled.compareAndSet(false, true)) {
@@ -82,7 +84,8 @@ actual open class MessageReceipt<C : Contact> actual constructor(
         if (_isRecalled.compareAndSet(false, true)) {
             return when (val contact = target) {
                 is QQ,
-                is Group -> contact.bot.recallIn(source, millis)
+                is Group,
+                -> contact.bot.recallIn(source, millis)
                 else -> error("Unknown contact type")
             }
         } else error("message is already or planned to be recalled")
@@ -92,6 +95,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      * [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
      * @see MessageChain.quote 引用一条消息
      */
+    @OptIn(ExperimentalMessageSource::class)
     actual open suspend fun quote(): QuoteReplyToSend {
         this.source.ensureSequenceIdAvailable()
         @OptIn(LowLevelAPI::class)
@@ -105,6 +109,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      *
      * @see MessageChain.quote 引用一条消息
      */
+    @OptIn(ExperimentalMessageSource::class)
     @LowLevelAPI
     @Suppress("FunctionName")
     actual fun _unsafeQuote(): QuoteReplyToSend {

+ 11 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt

@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.AddFriendResult
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.ExperimentalMessageSource
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSource
@@ -33,6 +34,7 @@ import kotlin.jvm.JvmSynthetic
 /**
  * 登录, 返回 [this]
  */
+@JvmSynthetic
 suspend inline fun <B : Bot> B.alsoLogin(): B = also { login() }
 // 任何人都能看到这个方法
 
@@ -167,6 +169,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
      * @see _lowLevelRecallFriendMessage 低级 API
      * @see _lowLevelRecallGroupMessage 低级 API
      */
+    @ExperimentalMessageSource
     @JvmSynthetic
     abstract suspend fun recall(source: MessageSource)
 
@@ -223,6 +226,7 @@ expect abstract class Bot() : CoroutineScope, LowLevelBotAPIAccessor {
  * @throws PermissionDeniedException 当 [Bot] 无权限操作时
  * @see Bot.recall
  */
+@JvmSynthetic
 suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[MessageSource])
 
 /**
@@ -233,6 +237,7 @@ suspend inline fun Bot.recall(message: MessageChain) = this.recall(message[Messa
  * @param coroutineContext 额外的 [CoroutineContext]
  * @see recall
  */
+@JvmSynthetic
 inline fun Bot.recallIn(
     source: MessageSource,
     millis: Long,
@@ -249,6 +254,7 @@ inline fun Bot.recallIn(
  * @param coroutineContext 额外的 [CoroutineContext]
  * @see recall
  */
+@JvmSynthetic
 inline fun Bot.recallIn(
     message: MessageChain,
     millis: Long,
@@ -265,15 +271,20 @@ inline fun Bot.recallIn(
  *
  * @param cause 原因. 为 null 时视为正常关闭, 非 null 时视为异常关闭
  */
+@JvmSynthetic
 suspend inline fun Bot.closeAndJoin(cause: Throwable? = null) {
     close(cause)
     coroutineContext[Job]?.join()
 }
 
+@JvmSynthetic
 inline fun Bot.containsFriend(id: Long): Boolean = this.friends.contains(id)
 
+@JvmSynthetic
 inline fun Bot.containsGroup(id: Long): Boolean = this.groups.contains(id)
 
+@JvmSynthetic
 inline fun Bot.getFriendOrNull(id: Long): QQ? = this.friends.getOrNull(id)
 
+@JvmSynthetic
 inline fun Bot.getGroupOrNull(id: Long): Group? = this.groups.getOrNull(id)

+ 2 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Contact.kt

@@ -57,6 +57,8 @@ expect abstract class Contact() : CoroutineScope, ContactJavaFriendlyAPI {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt

@@ -129,6 +129,8 @@ expect abstract class Group() : Contact, CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Member.kt

@@ -132,6 +132,8 @@ expect abstract class Member() : MemberJavaFriendlyAPI {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see MessageSendEvent.FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see MessageSendEvent.GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/QQ.kt

@@ -87,6 +87,8 @@ expect abstract class QQ() : Contact, CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 3 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/lowLevelApi.kt

@@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
 import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.QQ
 import net.mamoe.mirai.data.*
+import net.mamoe.mirai.message.MessageReceipt
 import net.mamoe.mirai.message.data.Message
 import net.mamoe.mirai.message.data.MessageSource
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
@@ -137,6 +138,7 @@ interface LowLevelBotAPIAccessor {
     /**
      * 获取群活跃信息
      */
+    @SinceMirai("0.29.0")
     @LowLevelAPI
     @MiraiExperimentalAPI
     suspend fun _lowLevelGetGroupActiveData(groupId: Long): GroupActiveData
@@ -147,7 +149,7 @@ interface LowLevelBotAPIAccessor {
     @SinceMirai("0.31.0")
     @LowLevelAPI
     @MiraiExperimentalAPI
-    suspend fun _lowLevelSendLongMessage(groupCode: Long, message: Message)
+    suspend fun _lowLevelSendLongGroupMessage(groupCode: Long, message: Message): MessageReceipt<Group>
 }
 
 /**

+ 2 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessagePacket.kt

@@ -7,7 +7,7 @@
  * https://github.com/mamoe/mirai/blob/master/LICENSE
  */
 
-@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE")
+@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS", "EXPERIMENTAL_API_USAGE", "unused")
 
 package net.mamoe.mirai.message
 
@@ -153,6 +153,7 @@ abstract class MessagePacketBase<TSender : QQ, TSubject : Contact> : Packet, Bot
     /**
      * 引用这个消息
      */
+    @ExperimentalMessageSource
     inline fun MessageChain.quote(): QuoteReplyToSend = this.quote(sender)
 
     operator fun <M : Message> get(at: Message.Key<M>): M {

+ 15 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/MessageReceipt.kt

@@ -15,12 +15,16 @@ import net.mamoe.mirai.LowLevelAPI
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.message.data.*
 import net.mamoe.mirai.recallIn
+import kotlin.jvm.JvmSynthetic
 
 /**
  * 发送消息后得到的回执. 可用于撤回.
  *
  * 此对象持有 [Contact] 的弱引用, [Bot] 离线后将会释放引用, 届时 [target] 将无法访问.
  *
+ * @param source 指代发送出去的消息
+ * @param target 消息发送对象
+ *
  * @see Group.sendMessage 发送群消息, 返回回执(此对象)
  * @see QQ.sendMessage 发送群消息, 返回回执(此对象)
  *
@@ -28,11 +32,15 @@ import net.mamoe.mirai.recallIn
  * @see MessageReceipt.sourceSequenceId 源序列号
  * @see MessageReceipt.sourceTime 源时间
  */
-expect open class MessageReceipt<C : Contact>(
+expect open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class) constructor(
     source: MessageSource,
     target: C,
     botAsMember: Member?
 ) {
+    /**
+     * 指代发送出去的消息
+     */
+    @ExperimentalMessageSource
     val source: MessageSource
 
     /**
@@ -90,6 +98,8 @@ expect open class MessageReceipt<C : Contact>(
  *
  * @see MessageSource.id
  */
+@get:JvmSynthetic
+@ExperimentalMessageSource
 inline val MessageReceipt<*>.sourceId: Long get() = this.source.id
 
 /**
@@ -97,6 +107,8 @@ inline val MessageReceipt<*>.sourceId: Long get() = this.source.id
  *
  * @see MessageSource.sequenceId
  */
+@get:JvmSynthetic
+@ExperimentalMessageSource
 inline val MessageReceipt<*>.sourceSequenceId: Int get() = this.source.sequenceId
 
 /**
@@ -104,6 +116,8 @@ inline val MessageReceipt<*>.sourceSequenceId: Int get() = this.source.sequenceI
  *
  * @see MessageSource.time
  */
+@get:JvmSynthetic
+@ExperimentalMessageSource
 inline val MessageReceipt<*>.sourceTime: Long get() = this.source.time
 
 suspend inline fun MessageReceipt<out Contact>.quoteReply(message: Message) {

+ 3 - 3
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/Image.kt

@@ -37,7 +37,7 @@ interface Image : Message, MessageContent {
      * 图片的 id. 只需要有这个 id 即可发送图片.
      *
      * 示例:
-     * 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f`
+     * 好友图片的 id: `/f8f1ab55-bf8e-4236-b55e-955848d7069f` 或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206`
      * 群图片的 id: `{01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png`
      */
     val imageId: String
@@ -53,7 +53,7 @@ interface Image : Message, MessageContent {
 @JsName("newImage")
 @JvmName("newImage")
 fun Image(imageId: String): Image = when (imageId.length) {
-    37 -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
+    54, 37 -> OfflineFriendImage(imageId) // /f8f1ab55-bf8e-4236-b55e-955848d7069f
     42 -> OfflineGroupImage(imageId) // {01E9451B-70ED-EAE3-B37C-101F1EEBF5B5}.png
     else -> throw IllegalArgumentException("Bad imageId, expecting length=37 or 42, got ${imageId.length}")
 }
@@ -211,7 +211,7 @@ abstract class OnlineGroupImage : GroupImage(), OnlineImage
 /**
  * 好友图片
  *
- * [imageId] 形如 `/f8f1ab55-bf8e-4236-b55e-955848d7069f` (37 长度)
+ * [imageId] 形如 `/f8f1ab55-bf8e-4236-b55e-955848d7069f` (37 长度)  或 `/000000000-3814297509-BFB7027B9354B8F899A062061D74E206` (54 长度)
  */ // NotOnlineImage
 @OptIn(MiraiInternalAPI::class)
 sealed class FriendImage : AbstractImage() {

+ 13 - 3
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageChain.kt

@@ -127,6 +127,7 @@ inline fun <reified M : Message> MessageChain.any(): Boolean = this.any { it is
 /**
  * 获取第一个 [M] 类型的 [Message] 实例
  */
+@OptIn(ExperimentalMessageSource::class)
 @JvmSynthetic
 @Suppress("UNCHECKED_CAST")
 fun <M : Message> MessageChain.firstOrNull(key: Message.Key<M>): M? = when (key) {
@@ -415,7 +416,10 @@ internal class MessageChainImplByIterable constructor(
 ) : Message, Iterable<SingleMessage>, MessageChain {
     override val size: Int by lazy { delegate.count() }
     override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
-    override fun toString(): String = this.delegate.joinToString("") { it.toString() }
+    var toStringTemp: String? = null
+    override fun toString(): String =
+        toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
+
     override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
 }
 
@@ -428,7 +432,10 @@ internal class MessageChainImplByCollection constructor(
 ) : Message, Iterable<SingleMessage>, MessageChain {
     override val size: Int get() = delegate.size
     override fun iterator(): Iterator<SingleMessage> = delegate.iterator()
-    override fun toString(): String = this.delegate.joinToString("") { it.toString() }
+    var toStringTemp: String? = null
+    override fun toString(): String =
+        toStringTemp ?: this.delegate.joinToString("") { it.toString() }.also { toStringTemp = it }
+
     override operator fun contains(sub: String): Boolean = delegate.any { it.contains(sub) }
 }
 
@@ -446,7 +453,10 @@ internal class MessageChainImplBySequence constructor(
      */
     private val collected: List<SingleMessage> by lazy { delegate.toList() }
     override fun iterator(): Iterator<SingleMessage> = collected.iterator()
-    override fun toString(): String = this.collected.joinToString("") { it.toString() }
+    var toStringTemp: String? = null
+    override fun toString(): String =
+        toStringTemp ?: this.collected.joinToString("") { it.toString() }.also { toStringTemp = it }
+
     override operator fun contains(sub: String): Boolean = collected.any { it.contains(sub) }
 }
 

+ 20 - 2
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/MessageSource.kt

@@ -19,6 +19,12 @@ import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.jvm.JvmSynthetic
 
+/**
+ * MessageSource 正计划于 0.32 或 0.33 或之后进行 API 不兼容的重写.
+ */
+@RequiresOptIn(message = "MessageSource 正计划于 0.32 或 0.33 或之后进行 API 不兼容的重写", level = RequiresOptIn.Level.WARNING)
+annotation class ExperimentalMessageSource
+
 /**
  * 消息源, 它存在于 [MessageChain] 中, 用于表示这个消息的来源.
  *
@@ -29,6 +35,7 @@ import kotlin.jvm.JvmSynthetic
  * @see Bot.recall 撤回一条消息
  * @see MessageSource.quote 引用这条消息, 创建 [MessageChain]
  */
+@ExperimentalMessageSource
 interface MessageSource : Message, MessageMetadata {
     companion object Key : Message.Key<MessageSource>
 
@@ -82,6 +89,7 @@ interface MessageSource : Message, MessageMetadata {
  * 序列号. 若是机器人发出去的消息, 请先 [确保 sequenceId 可用][MessageSource.ensureSequenceIdAvailable]
  * @see MessageSource.id
  */
+@ExperimentalMessageSource
 @get:JvmSynthetic
 inline val MessageSource.sequenceId: Int
     get() = (this.id shr 32).toInt()
@@ -90,6 +98,7 @@ inline val MessageSource.sequenceId: Int
  * 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
  * @see MessageSource.id
  */
+@ExperimentalMessageSource
 @get:JvmSynthetic
 inline val MessageSource.messageRandom: Int
     get() = this.id.toInt()
@@ -98,24 +107,33 @@ inline val MessageSource.messageRandom: Int
 
 /**
  * 消息 id.
+ *
+ * 仅接收到的消息才可以获取这个 id.
+ *
  * @see MessageSource.id
  */
+@ExperimentalMessageSource
 @get:JvmSynthetic
 inline val MessageChain.id: Long
     get() = this[MessageSource].id
 
 /**
  * 消息序列号, 可能来自服务器也可以发送时赋值, 不唯一.
+ *
+ * 仅接收到的消息才可以获取这个序列号.
+ *
  * @see MessageSource.id
  */
+@ExperimentalMessageSource
 @get:JvmSynthetic
 inline val MessageChain.sequenceId: Int
-    get() = this[MessageSource].sequenceId
+    get() = this.getOrNull(MessageSource)?.sequenceId ?: error("Only MessageChain from server has sequenceId")
 
 /**
  * 消息随机数. 由服务器或客户端指定后不能更改. 它是消息 id 的一部分.
  * @see MessageSource.id
  */
+@ExperimentalMessageSource
 @get:JvmSynthetic
 inline val MessageChain.messageRandom: Int
-    get() = this[MessageSource].messageRandom
+    get() = this.getOrNull(MessageSource)?.messageRandom ?: error("Only MessageChain from server has sequenceId")

+ 4 - 1
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/QuoteReply.kt

@@ -29,6 +29,7 @@ import kotlin.jvm.JvmName
  * 总是使用 [quote] 来构造这个实例.
  */
 open class QuoteReply
+@OptIn(ExperimentalMessageSource::class)
 @MiraiInternalAPI constructor(val source: MessageSource) : Message, MessageMetadata {
     companion object Key : Message.Key<QuoteReply>
 
@@ -39,7 +40,7 @@ open class QuoteReply
  * 用于发送的引用回复.
  * 总是使用 [quote] 来构造实例.
  */
-@OptIn(MiraiInternalAPI::class)
+@OptIn(MiraiInternalAPI::class, ExperimentalMessageSource::class)
 sealed class QuoteReplyToSend
 @MiraiInternalAPI constructor(source: MessageSource) : QuoteReply(source) {
     class ToGroup(source: MessageSource, val sender: QQ) : QuoteReplyToSend(source) {
@@ -53,6 +54,7 @@ sealed class QuoteReplyToSend
  * 引用这条消息.
  * @see sender 消息发送人.
  */
+@ExperimentalMessageSource
 @OptIn(MiraiInternalAPI::class)
 fun MessageChain.quote(sender: QQ?): QuoteReplyToSend {
     this.firstOrNull<MessageSource>()?.let {
@@ -65,6 +67,7 @@ fun MessageChain.quote(sender: QQ?): QuoteReplyToSend {
  * 引用这条消息.
  * @see from 消息来源. 若是好友发送
  */
+@ExperimentalMessageSource
 @OptIn(MiraiInternalAPI::class)
 fun MessageSource.quote(from: QQ?): QuoteReplyToSend {
     return if (this.groupId != 0L) {

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/Bot.kt

@@ -7,6 +7,7 @@ import kotlinx.coroutines.io.ByteReadChannel
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.AddFriendResult
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.ExperimentalMessageSource
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSource
@@ -162,6 +163,7 @@ actual abstract class Bot actual constructor() : CoroutineScope, LowLevelBotAPIA
      * @see _lowLevelRecallFriendMessage 低级 API
      * @see _lowLevelRecallGroupMessage 低级 API
      */
+    @ExperimentalMessageSource
     @JvmSynthetic
     actual abstract suspend fun recall(source: MessageSource)
 

+ 4 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/BotJavaFriendlyAPI.kt

@@ -5,6 +5,7 @@ import net.mamoe.mirai.contact.PermissionDeniedException
 import net.mamoe.mirai.contact.recall
 import net.mamoe.mirai.data.AddFriendResult
 import net.mamoe.mirai.message.MessageReceipt
+import net.mamoe.mirai.message.data.ExperimentalMessageSource
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.MessageSource
@@ -61,6 +62,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
      *
      * @see Bot.recall (扩展函数) 接受参数 [MessageChain]
      */
+    @ExperimentalMessageSource
     @JvmName("recall")
     fun __recallBlockingForJava__(source: MessageSource) {
         runBlocking { recall(source) }
@@ -88,6 +90,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
      * @param millis 延迟的时间, 单位为毫秒
      * @see recall
      */
+    @ExperimentalMessageSource
     @JvmName("recallIn")
     fun __recallIn_MemberForJava__(source: MessageSource, millis: Long) {
         runBlocking { recallIn(source, millis) }
@@ -148,6 +151,7 @@ actual abstract class BotJavaFriendlyAPI actual constructor() {
     /**
      * 异步调用 [__recallBlockingForJava__]
      */
+    @ExperimentalMessageSource
     @JvmName("recallAsync")
     fun __recallAsyncForJava__(source: MessageSource): Future<Unit> {
         return future { recall(source) }

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Contact.kt

@@ -53,6 +53,8 @@ actual abstract class Contact : CoroutineScope, ContactJavaFriendlyAPI() {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/ContactJavaFriendlyAPI.kt

@@ -48,6 +48,8 @@ actual abstract class ContactJavaFriendlyAPI {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Group.kt

@@ -127,6 +127,8 @@ actual abstract class Group : Contact(), CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/Member.kt

@@ -118,6 +118,8 @@ actual abstract class Member : MemberJavaFriendlyAPI() {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 2 - 0
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/contact/QQ.kt

@@ -78,6 +78,8 @@ actual abstract class QQ : Contact(), CoroutineScope {
     /**
      * 向这个对象发送消息.
      *
+     * 单条消息最大可发送 4500 字符或 50 张图片.
+     *
      * @see FriendMessageSendEvent 发送好友信息事件, cancellable
      * @see GroupMessageSendEvent  发送群消息事件. cancellable
      *

+ 5 - 1
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/MessageReceipt.kt

@@ -29,7 +29,8 @@ import net.mamoe.mirai.utils.unsafeWeakRef
  */
 @Suppress("FunctionName")
 @OptIn(MiraiInternalAPI::class)
-actual open class MessageReceipt<C : Contact> actual constructor(
+actual open class MessageReceipt<C : Contact> @OptIn(ExperimentalMessageSource::class)
+actual constructor(
     actual val source: MessageSource,
     target: C,
     private val botAsMember: Member?
@@ -56,6 +57,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      * @see Bot.recall
      * @throws IllegalStateException 当此消息已经被撤回或正计划撤回时
      */
+    @OptIn(ExperimentalMessageSource::class)
     actual suspend fun recall() {
         @Suppress("BooleanLiteralArgument")
         if (_isRecalled.compareAndSet(false, true)) {
@@ -84,6 +86,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      * [确保 sequenceId可用][MessageSource.ensureSequenceIdAvailable] 然后引用这条消息.
      * @see MessageChain.quote 引用一条消息
      */
+    @OptIn(ExperimentalMessageSource::class)
     actual open suspend fun quote(): QuoteReplyToSend {
         this.source.ensureSequenceIdAvailable()
         @OptIn(LowLevelAPI::class)
@@ -97,6 +100,7 @@ actual open class MessageReceipt<C : Contact> actual constructor(
      *
      * @see MessageChain.quote 引用一条消息
      */
+    @OptIn(ExperimentalMessageSource::class)
     @LowLevelAPI
     @Suppress("FunctionName")
     actual fun _unsafeQuote(): QuoteReplyToSend {

+ 1 - 1
mirai-core/src/jvmMain/kotlin/net/mamoe/mirai/message/SendImageUtilsJvm.kt

@@ -116,7 +116,7 @@ suspend fun InputStream.uploadAsImage(contact: Contact): OfflineImage =
  */
 @Throws(OverFileSizeMaxException::class)
 suspend fun File.uploadAsImage(contact: Contact): OfflineImage {
-    require(this.exists() && this.canRead())
+    require(this.isFile && this.exists() && this.canRead()) { "file ${this.path} is not readable" }
     return withContext(Dispatchers.IO) { toExternalImage() }.upload(contact)
 }
 

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません