Explorar el Código

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

Him188 hace 5 años
padre
commit
333103eca7

+ 2 - 1
binary-compatibility-validator/api/binary-compatibility-validator.api

@@ -4706,12 +4706,13 @@ public final class net/mamoe/mirai/message/data/MusicKind : java/lang/Enum {
 	public static fun values ()[Lnet/mamoe/mirai/message/data/MusicKind;
 }
 
-public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/message/data/ConstrainSingle, net/mamoe/mirai/message/data/MessageContent {
+public final class net/mamoe/mirai/message/data/MusicShare : net/mamoe/mirai/message/code/CodableMessage, 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 static final field SERIAL_NAME Ljava/lang/String;
 	public synthetic fun <init> (ILnet/mamoe/mirai/message/data/MusicKind;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
 	public fun <init> (Lnet/mamoe/mirai/message/data/MusicKind;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
 	public fun <init> (Lnet/mamoe/mirai/message/data/MusicKind;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+	public fun appendMiraiCodeTo (Ljava/lang/StringBuilder;)V
 	public final fun component1 ()Lnet/mamoe/mirai/message/data/MusicKind;
 	public final fun component2 ()Ljava/lang/String;
 	public final fun component3 ()Ljava/lang/String;

+ 0 - 1
build.gradle.kts

@@ -104,7 +104,6 @@ allprojects {
         maven(url = "https://kotlin.bintray.com/kotlinx")
         google()
         mavenCentral()
-        maven(url = "https://dl.bintray.com/karlatemp/misc")
     }
 
     afterEvaluate {

+ 2 - 2
buildSrc/src/main/kotlin/Versions.kt

@@ -12,7 +12,7 @@
 import org.gradle.api.attributes.Attribute
 
 object Versions {
-    const val project = "2.5.0-dev-1"
+    const val project = "2.5.0-dev-2"
 
     const val core = project
     const val console = project
@@ -103,4 +103,4 @@ const val yamlkt = "net.mamoe.yamlkt:yamlkt:${Versions.yamlkt}"
 const val `jetbrains-annotations` = "org.jetbrains:annotations:19.0.0"
 
 
-const val `caller-finder` = "io.github.karlatemp:caller:1.0.1"
+const val `caller-finder` = "io.github.karlatemp:caller:1.1.1"

+ 52 - 3
docs/Bots.md

@@ -68,6 +68,25 @@ workingDir = File("C:/mirai")
 setWorkingDir(File("C:/mirai"))
 ```
 
+#### 修改缓存目录
+
+缓存目录会相对于 `workingDir` 解析。如 `File("cache")` 将会解析为 `workingDir` 内的 `cache` 目录。而 `File("C:/cache")` 将会解析为绝对的 `C:/cache` 目录。
+
+默认为 `File("cache")`
+
+要修改缓存目录(自 mirai 2.4.0):
+```
+// Kotlin
+cacheDir = File("cache") // 最终为 workingDir 目录中的 cache 目录
+cacheDir = File("C:/cache") // 最终为 C:/cache
+
+// Java
+setCacheDir(File("cache")) // 最终为 workingDir 目录中的 cache 目录
+setCacheDir(File("C:/cache")) // 最终为 C:/cache
+```
+
+目前缓存目录会存储列表缓存、登录服务器、资源会话秘钥等。这些数据的存储方式有可能变化,请不要修改缓存目录中的文件。
+
 #### 设备信息
 Bot 默认使用全随机的设备信息。**在更换账号地点时候使用随机设备信息可能会导致无法登录**,当然,**成功登录时使用的设备信息也可以保存后在新的设备使用**。
 
@@ -102,7 +121,6 @@ protocol = BotConfiguration.MiraiProtocol.ANDROID_PAD
 // Java
 setProtocol(MiraiProtocol.ANDROID_PAD)
 ```
-
 #### 重定向日志
 Bot 有两个日志类别,`Bot` 或 `Net`。`Bot` 为通常日志,如收到事件。`Net` 为网络日志,包含收到和发出的每一个包和网络层解析时遇到的错误。
 
@@ -149,6 +167,34 @@ setLoginSolver(new YourLoginSolver())
 
 > 要获取更多有关 `LoginSolver` 的信息,查看 [LoginSolver.kt](../mirai-core-api/src/commonMain/kotlin/utils/LoginSolver.kt#L32)
 
+#### 启用列表缓存
+Mirai 在启动时会拉取全部好友列表和群成员列表。当账号拥有过多群时登录可能缓慢,开启列表缓存会大幅加速登录过程。
+
+Mirai 自动根据事件更新列表,并在每次登录时与服务器校验缓存有效性,**但有时候可能发生意外情况导致列表没有同步。如果出现找不到群员或好友等不同步情况,请关闭缓存并[提交 Bug](https://github.com/mamoe/mirai/issues/new?assignees=&labels=question&template=bug.md)**
+
+要开启列表缓存(自 mirai 2.4.0):
+```
+// 开启所有列表缓存
+enableContactCache()
+```
+
+也可以只开启部分缓存:
+```
+// Kotlin
+contactListCache {
+    friendListCacheEnabled = true // 开启好友列表缓存
+    groupMemberListCacheEnabled = true // 开启群成员列表缓存
+    
+    saveIntervalMillis = 60_000 // 可选设置有更新时的保存时间间隔, 默认 60 秒
+}
+
+// Java
+contactListCache.setFriendListCacheEnabled(true) // 开启好友列表缓存
+contactListCache.setGroupMemberListCacheEnabled(true) // 开启群成员列表缓存
+contactListCache.setSaveIntervalMillis(60000) // 可选设置有更新时的保存时间间隔, 默认 60 秒
+```
+
+
 ### 获取当前所有 `Bot` 实例
 
 在登录后 `Bot` 实例会被自动记录。可在 `Bot.instances` 获取到当前**在线**的所有 `Bot` 列表。
@@ -168,11 +214,14 @@ setLoginSolver(new YourLoginSolver())
 
 ### 常见登录失败原因
 
+[#993]: https://github.com/mamoe/mirai/discussions/993
+
 | 错误信息       | 可能的原因        | 可能的解决方案          |
 |:--------------|:---------------|:----------------------|
-| 当前版本过低    | 密码错误         | 检查密码                |
-| 当前上网环境异常 | 设备锁           | 开启或关闭设备锁后重试登录 |
+| 当前版本过低    | 密码错误         | 检查密码或修改密码到 16 位以内                |
+| 当前上网环境异常 | 设备锁           | 开启或关闭设备锁 (登录保护) |
 | 禁止登录       | 需要处理滑块验证码 | [project-mirai/mirai-login-solver-selenium] |
+| 密码错误       | 密码错误或过长 | 手机协议最大支持 16 位密码 ([#993]). 在官方 PC 客户端登录后修改密码 |
 
 若以上方案无法解决问题,请尝试 [切换登录协议](#切换登录协议) 和 **[处理滑动验证码](#处理滑动验证码)**。
 

+ 2 - 0
mirai-core-api/src/commonMain/kotlin/event/MessageSubscribersBuilder.kt

@@ -475,6 +475,7 @@ public open class MessageSubscribersBuilder<M : MessageEvent, out Ret, R : RR, R
     internal suspend inline fun executeAndReply(m: M, replier: suspend M.(String) -> Any?): RR {
         when (val message = replier(m, m.message.contentToString())) {
             is Message -> m.subject.sendMessage(message)
+            null,
             is Unit -> Unit
             else -> m.subject.sendMessage(message.toString())
         }
@@ -485,6 +486,7 @@ public open class MessageSubscribersBuilder<M : MessageEvent, out Ret, R : RR, R
     internal suspend inline fun executeAndQuoteReply(m: M, replier: suspend M.(String) -> Any?): RR {
         when (val message = replier(m, m.message.contentToString())) {
             is Message -> m.subject.sendMessage(m.message.quote() + message)
+            null,
             is Unit -> Unit
             else -> m.subject.sendMessage(m.message.quote() + message.toString())
         }

+ 69 - 11
mirai-core-api/src/commonMain/kotlin/message/code/internal/impl.kt

@@ -25,13 +25,7 @@ internal fun String.parseMiraiCodeImpl(contact: Contact?): MessageChain = buildM
             add(PlainText(origin.decodeMiraiCode()))
             return@forEachMiraiCode
         }
-        parser.argsRegex.matchEntire(args)
-            ?.destructured
-            ?.let {
-                parser.runCatching {
-                    contact.mapper(it)
-                }.getOrNull()
-            }
+        parser.parse(contact, args)
             ?.let(::add)
             ?: add(PlainText(origin.decodeMiraiCode()))
     }
@@ -128,13 +122,77 @@ private object MiraiCodeParsers : Map<String, MiraiCodeParser> by mapOf(
     "dice" to MiraiCodeParser(Regex("""([1-6])""")) { (value) ->
         Dice(value.toInt())
     },
-)
+    "musicshare" to MiraiCodeParser.DynamicParser(7) { args ->
+        val (kind, title, summary, jumpUrl, pictureUrl) = args
+        val musicUrl = args[5]
+        val brief = args[6]
 
-private class MiraiCodeParser(
-    val argsRegex: Regex,
-    val mapper: Contact?.(MatchResult.Destructured) -> Message?
+        MusicShare(MusicKind.valueOf(kind), title, summary, jumpUrl, pictureUrl, musicUrl, brief)
+    },
 )
 
+
+// Visitable for test
+internal sealed class MiraiCodeParser {
+    abstract fun parse(contact: Contact?, args: String): Message?
+    class RegexParser(
+        private val argsRegex: Regex,
+        private val mapper: Contact?.(MatchResult.Destructured) -> Message?
+    ) : MiraiCodeParser() {
+        override fun parse(contact: Contact?, args: String): Message? =
+            argsRegex.matchEntire(args)
+                ?.destructured
+                ?.let {
+                    runCatching {
+                        contact.mapper(it)
+                    }.getOrNull()
+                }
+    }
+
+    class DynamicParser(
+        private val minArgs: Int,
+        private val maxArgs: Int = minArgs,
+        private val parser: (Contact?.(args: Array<String>) -> Message?),
+    ) : MiraiCodeParser() {
+        override fun parse(contact: Contact?, args: String): Message? {
+            val ranges = mutableListOf<IntRange>()
+            if (args.isNotEmpty()) {
+                var begin = 0
+                var pos = 0
+                val len = args.length
+                while (pos < len) {
+                    when (args[pos]) {
+                        '\\' -> pos += 2
+                        ',' -> {
+                            ranges.add(begin..pos)
+                            pos++
+                            begin = pos
+                        }
+                        else -> pos++
+                    }
+                }
+                ranges.add(begin..len)
+            }
+            if (ranges.size < minArgs) return null
+            if (ranges.size > maxArgs) return null
+            @Suppress("RemoveExplicitTypeArguments")
+            val args0 = Array<String>(ranges.size) { index ->
+                val range = ranges[index]
+                args.substring(range.first, range.last).decodeMiraiCode()
+            }
+            runCatching {
+                return parser(contact, args0)
+            }
+            return null
+        }
+    }
+}
+
+private fun MiraiCodeParser(
+    argsRegex: Regex,
+    mapper: Contact?.(MatchResult.Destructured) -> Message?
+): MiraiCodeParser = MiraiCodeParser.RegexParser(argsRegex, mapper)
+
 internal fun StringBuilder.appendStringAsMiraiCode(value: String): StringBuilder = apply {
     value.forEach { char ->
         when (char) {

+ 16 - 1
mirai-core-api/src/commonMain/kotlin/message/data/MusicShare.kt

@@ -13,6 +13,8 @@ package net.mamoe.mirai.message.data
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import net.mamoe.mirai.message.code.CodableMessage
+import net.mamoe.mirai.message.code.internal.appendStringAsMiraiCode
 import net.mamoe.mirai.utils.MiraiExperimentalApi
 import net.mamoe.mirai.utils.MiraiInternalApi
 import net.mamoe.mirai.utils.safeCast
@@ -53,7 +55,7 @@ public data class MusicShare(
      * 在消息列表显示
      */
     public val brief: String,
-) : MessageContent, ConstrainSingle {
+) : MessageContent, ConstrainSingle, CodableMessage {
 
     public constructor(
         /**
@@ -88,6 +90,19 @@ public data class MusicShare(
     override fun contentToString(): String =
         brief.takeIf { it.isNotBlank() } ?: "[分享]$title" // empty content is not accepted by `sendMessage`
 
+    override fun appendMiraiCodeTo(builder: StringBuilder) {
+        builder.append("[mirai:musicshare:")
+            .append(kind.name)
+            .append(',').appendStringAsMiraiCode(title)
+            .append(',').appendStringAsMiraiCode(summary)
+            .append(',').appendStringAsMiraiCode(jumpUrl)
+            .append(',').appendStringAsMiraiCode(pictureUrl)
+            .append(',').appendStringAsMiraiCode(musicUrl)
+            .append(',').appendStringAsMiraiCode(brief)
+            .append(']')
+    }
+
+
     // 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')
 
     /**

+ 2 - 2
mirai-core-api/src/commonMain/kotlin/utils/BotConfiguration.kt

@@ -277,8 +277,8 @@ public open class BotConfiguration { // open for Java
      *
      * - 默认打印到标准输出, 通过 [MiraiLogger.create]
      * - 忽略所有日志: [noBotLog]
-     * - 重定向到一个目录: `networkLoggerSupplier = { DirectoryLogger("Net ${it.id}") }`
-     * - 重定向到一个文件: `networkLoggerSupplier = { SingleFileLogger("Net ${it.id}") }`
+     * - 重定向到一个目录: `botLoggerSupplier = { DirectoryLogger("Bot ${it.id}") }`
+     * - 重定向到一个文件: `botLoggerSupplier = { SingleFileLogger("Bot ${it.id}") }`
      *
      * @see MiraiLogger
      */

+ 27 - 0
mirai-core-api/src/commonTest/kotlin/message/code/TestMiraiCode.kt

@@ -10,11 +10,27 @@
 package net.mamoe.mirai.message.code
 
 import net.mamoe.mirai.message.code.MiraiCode.deserializeMiraiCode
+import net.mamoe.mirai.message.code.internal.MiraiCodeParser
 import net.mamoe.mirai.message.data.*
 import org.junit.jupiter.api.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
 
 class TestMiraiCode {
+    @Test
+    fun testDynamicMiraiCodeParser() {
+        fun runTest(args: Int, code: String, parse: (args: Array<String>) -> Unit) {
+            val response = MiraiCodeParser.DynamicParser(args) { args0 -> parse(args0); AtAll }.parse(null, code)
+            assertNotNull(response, "Parser not invoked")
+        }
+        runTest(3, "test,\\,test,\\,\\,test") { (arg1, arg2, arg3) ->
+            assertEquals("test", arg1)
+            assertEquals(",test", arg2)
+            assertEquals(",,test", arg3)
+        }
+        runTest(2, ",") {}
+    }
+
     @Test
     fun testCodes() {
         assertEquals(AtAll.toMessageChain(), "[mirai:atall]".deserializeMiraiCode())
@@ -46,5 +62,16 @@ class TestMiraiCode {
         assertEquals(buildMessageChain {
             +Dice(1)
         }, "[mirai:dice:1]".deserializeMiraiCode())
+
+        val musicShare = MusicShare(
+            kind = MusicKind.NeteaseCloudMusic,
+            title = "ファッション",
+            summary = "rinahamu/Yunomi",
+            jumpUrl = "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",
+            brief = "",
+        )
+        assertEquals(musicShare.toMessageChain(), musicShare.serializeToMiraiCode().deserializeMiraiCode())
     }
 }

+ 5 - 6
mirai-core/src/commonMain/kotlin/AbstractBot.kt

@@ -292,13 +292,12 @@ internal abstract class AbstractBot<N : BotNetworkHandler> constructor(
 
             // https://github.com/mamoe/mirai/issues/1019
             kotlin.runCatching {
-                nick
+                bot.nick
             }.onFailure {
-                throw contextualBugReportException(
-                    context = "Bot login",
-                    forDebug = it.toString(),
-                    e = it,
-                )
+                bot.asQQAndroidBot().nick = MiraiImpl.queryProfile(bot, bot.id).nickname
+                if (bot.nick.isBlank()) {
+                    logger.warning { "Unable to fetch nickname of bot." }
+                }
             }
 
             logger.info { "Login successful" }

+ 3 - 1
mirai-core/src/commonMain/kotlin/MiraiImpl.kt

@@ -20,7 +20,6 @@ import kotlinx.coroutines.withContext
 import kotlinx.io.core.discardExact
 import kotlinx.io.core.readBytes
 import kotlinx.serialization.json.*
-import net.mamoe.kjbb.JvmBlockingBridge
 import net.mamoe.mirai.*
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.*
@@ -794,6 +793,9 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
                 accept = accept,
                 blackList = blackList
             ).sendWithoutExpect()
+
+            if (!accept) return@apply
+
             @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
             bot.friends.delegate.add(newFriend(bot, FriendInfoImpl(fromId, fromNick, "")))
         }

+ 31 - 5
mirai-core/src/commonMain/kotlin/message/ReceiveMessageHandler.kt

@@ -156,6 +156,33 @@ private object ReceiveMessageTransformer {
         }
     }
 
+    fun MessageChainBuilder.compressContinuousPlainText() {
+        var index = 0
+        val builder = StringBuilder()
+        while (index + 1 < size) {
+            val elm0 = get(index)
+            val elm1 = get(index + 1)
+            if (elm0 is PlainText && elm1 is PlainText) {
+                builder.setLength(0)
+                var end = -1
+                for (i in index until size) {
+                    val elm = get(i)
+                    if (elm is PlainText) {
+                        end = i
+                        builder.append(elm.content)
+                    } else break
+                }
+                set(index, PlainText(builder.toString()))
+                // do delete
+                val index1 = index + 1
+                repeat(end - index) {
+                    removeAt(index1)
+                }
+            }
+            index++
+        }
+    }
+
     fun MessageChain.cleanupRubbishMessageElements(): MessageChain {
         var previousLast: SingleMessage? = null
         var last: SingleMessage? = null
@@ -215,15 +242,14 @@ private object ReceiveMessageTransformer {
                     }
                 }
 
-                if (element is PlainText) { // 处理分片消息
-                    append(element.content)
-                } else {
-                    add(element)
-                }
+                append(element)
 
                 previousLast = last
                 last = element
             }
+
+            // 处理分片信息
+            compressContinuousPlainText()
         }
     }
 

+ 5 - 2
mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt

@@ -165,6 +165,8 @@ internal open class QQAndroidClient(
 
     class MessageSvcSyncData {
         val firstNotify: AtomicBoolean = atomic(true)
+        var latestMsgNewGroupTime: Long = currentTimeSeconds()
+        var latestMsgNewFriendTime: Long = currentTimeSeconds()
 
         @Volatile
         var syncCookie: ByteArray? = null
@@ -180,12 +182,13 @@ internal open class QQAndroidClient(
 
         val pbGetMessageCacheList = SyncingCacheList<PbGetMessageSyncId>()
 
-        internal data class SystemMsgNewGroupSyncId(
+        internal data class SystemMsgNewSyncId(
             val sequence: Long,
             val time: Long
         )
 
-        val systemMsgNewGroupCacheList = SyncingCacheList<SystemMsgNewGroupSyncId>(10)
+        val systemMsgNewGroupCacheList = SyncingCacheList<SystemMsgNewSyncId>(10)
+        val systemMsgNewFriendCacheList = SyncingCacheList<SystemMsgNewSyncId>(10)
 
 
         internal data class PbPushTransMsgSyncId(

+ 65 - 24
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/NewContact.kt

@@ -19,6 +19,7 @@ import net.mamoe.mirai.event.events.MemberJoinRequestEvent
 import net.mamoe.mirai.event.events.NewFriendRequestEvent
 import net.mamoe.mirai.internal.QQAndroidBot
 import net.mamoe.mirai.internal.message.contextualBugReportException
+import net.mamoe.mirai.internal.network.MultiPacketByIterable
 import net.mamoe.mirai.internal.network.Packet
 import net.mamoe.mirai.internal.network.QQAndroidClient
 import net.mamoe.mirai.internal.network.protocol.data.proto.Structmsg
@@ -27,11 +28,12 @@ import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.internal.utils._miraiContentToString
 import net.mamoe.mirai.internal.utils.io.serialization.loadAs
 import net.mamoe.mirai.internal.utils.io.serialization.writeProtoBuf
+import kotlin.math.max
 
 internal class NewContact {
 
     internal object SystemMsgNewFriend :
-        OutgoingPacketFactory<NewFriendRequestEvent?>("ProfileService.Pb.ReqSystemMsgNew.Friend") {
+        OutgoingPacketFactory<Packet?>("ProfileService.Pb.ReqSystemMsgNew.Friend") {
 
         operator fun invoke(client: QQAndroidClient) = buildOutgoingUniPacket(client) {
             writeProtoBuf(
@@ -55,18 +57,40 @@ internal class NewContact {
         }
 
 
-        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): NewFriendRequestEvent? {
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? {
             readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run {
-                val struct = friendmsgs.firstOrNull()// 会有重复且无法过滤, 不要用 map
-                return struct?.msg?.run {
-                    NewFriendRequestEvent(
-                        bot,
-                        struct.msgSeq,
-                        msgAdditional,
-                        struct.reqUin,
-                        groupCode,
-                        reqUinNick
-                    )
+                return friendmsgs.filter {
+                    it.msgTime >= bot.client.syncingController.latestMsgNewFriendTime
+                }.mapNotNull { struct ->
+                    if (!bot.client.syncingController.systemMsgNewFriendCacheList.addCache(
+                            QQAndroidClient.MessageSvcSyncData.SystemMsgNewSyncId(
+                                struct.msgSeq,
+                                struct.msgTime
+                            )
+                        )
+                    ) { // duplicate
+                        return@mapNotNull null
+                    }
+                    struct.msg?.run {
+                        NewFriendRequestEvent(
+                            bot,
+                            struct.msgSeq,
+                            msgAdditional,
+                            struct.reqUin,
+                            groupCode,
+                            reqUinNick
+                        )
+                    }
+                }.let { packets ->
+                    when {
+                        packets.isEmpty() -> null
+                        packets.size == 1 -> packets[0]
+                        else -> MultiPacketByIterable(packets)
+                    }
+                }.also {
+                    bot.client.syncingController.run {
+                        latestMsgNewFriendTime = max(latestMsgNewFriendTime, friendmsgs.maxOfOrNull { it.msgTime } ?: 0)
+                    }
                 }
             }
         }
@@ -143,18 +167,8 @@ internal class NewContact {
 
 
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Packet? {
-            return readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run {
-                val struct = groupmsgs.firstOrNull() ?: return null // 会有重复且无法过滤, 不要用 map
-
-                if (!bot.client.syncingController.systemMsgNewGroupCacheList.addCache(
-                        QQAndroidClient.MessageSvcSyncData.SystemMsgNewGroupSyncId(struct.msgSeq, struct.msgTime)
-                    )
-                ) { // duplicate
-                    return null
-                }
-
-                struct.msg?.run {
-                    //this.soutv("SystemMsg")
+            fun handleStruct(struct: Structmsg.StructMsg): Packet? {
+                return struct.msg?.run {
                     when (subType) {
                         1 -> { // 处理被邀请入群 或 处理成员入群申请
                             when (groupMsgType) {
@@ -232,6 +246,33 @@ internal class NewContact {
                     }
                 }
             }
+
+            return readBytes().loadAs(Structmsg.RspSystemMsgNew.serializer()).run {
+                groupmsgs.filter {
+                    it.msgTime >= bot.client.syncingController.latestMsgNewGroupTime
+                }.mapNotNull { struct ->
+                    if (!bot.client.syncingController.systemMsgNewGroupCacheList.addCache(
+                            QQAndroidClient.MessageSvcSyncData.SystemMsgNewSyncId(
+                                struct.msgSeq,
+                                struct.msgTime
+                            )
+                        )
+                    ) { // duplicate
+                        return@mapNotNull null
+                    }
+                    handleStruct(struct)
+                }.let { packets ->
+                    when {
+                        packets.isEmpty() -> null
+                        packets.size == 1 -> packets[0]
+                        else -> MultiPacketByIterable(packets)
+                    }
+                }.also {
+                    bot.client.syncingController.run {
+                        latestMsgNewGroupTime = max(latestMsgNewGroupTime, groupmsgs.maxOfOrNull { it.msgTime } ?: 0)
+                    }
+                }
+            }
         }
 
         internal object Action : OutgoingPacketFactory<Nothing?>("ProfileService.Pb.ReqSystemMsgAction.Group") {

+ 5 - 1
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/receive/OnlinePush.PbPushGroupMsg.kt

@@ -156,7 +156,11 @@ internal object OnlinePushPbPushGroupMsg : IncomingPacketFactory<Packet?>("Onlin
     ) = when {
         flags and 16 != 0 -> MemberPermission.ADMINISTRATOR
         flags and 8 != 0 -> MemberPermission.OWNER
-        flags == 0 || flags == 1 -> MemberPermission.MEMBER
+        (when (flags) {
+            1, 0, 64,
+            -> true
+            else -> false
+        }) -> MemberPermission.MEMBER
         else -> {
             bot.logger.warning { "判断群 ${sender.group.id} 的群员 ${sender.id} 的权限失败: ${flags._miraiContentToString()}. 请完整截图或复制此日志并确认其真实权限后发送给 mirai 维护者以帮助解决问题." }
             sender.permission