Răsfoiți Sursa

Support detect official bot, fix #439 (#810)

* Support detect official bot, fix #439

* Fix wrong command name and add missed value

* Adapt to changed invite event

* Internal MessageData

* Fix build

* Fix wording
sandtechnology 5 ani în urmă
părinte
comite
3a5d707417

+ 5 - 0
mirai-core-api/src/commonMain/kotlin/data/MemberInfo.kt

@@ -33,4 +33,9 @@ public interface MemberInfo : UserInfo {
      * 上次发言时间 秒
      */
     public val lastSpeakTimestamp: Int
+
+    /**
+     * 是否为官方机器人
+     */
+    public val isOfficialBot: Boolean
 }

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

@@ -328,7 +328,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
                     nextUin = nextUin
                 ).sendAndExpect<FriendList.GetTroopMemberList.Response>(retry = 3)
                 sequence += data.members.asSequence().map { troopMemberInfo ->
-                    MemberInfoImpl(troopMemberInfo, ownerId)
+                    MemberInfoImpl(bot.client, troopMemberInfo, ownerId)
                 }
                 nextUin = data.nextUin
                 if (nextUin == 0L) {

+ 6 - 2
mirai-core/src/commonMain/kotlin/contact/MemberInfoImpl.kt

@@ -11,6 +11,7 @@ package net.mamoe.mirai.internal.contact
 
 import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.data.MemberInfo
+import net.mamoe.mirai.internal.network.QQAndroidClient
 import net.mamoe.mirai.internal.network.protocol.data.jce.StTroopMemberInfo
 import net.mamoe.mirai.utils.currentTimeSeconds
 
@@ -24,9 +25,11 @@ internal class MemberInfoImpl(
     override val muteTimestamp: Int,
     override val anonymousId: String?,
     override val joinTimestamp: Int = currentTimeSeconds().toInt(),
-    override var lastSpeakTimestamp: Int = 0
+    override var lastSpeakTimestamp: Int = 0,
+    override val isOfficialBot: Boolean = false
 ) : MemberInfo, UserInfoImpl(uin, nick, remark) {
     constructor(
+        client: QQAndroidClient,
         jceInfo: StTroopMemberInfo,
         groupOwnerId: Long
     ) : this(
@@ -43,6 +46,7 @@ internal class MemberInfoImpl(
         muteTimestamp = jceInfo.dwShutupTimestap?.toInt() ?: 0,
         anonymousId = null,
         joinTimestamp = jceInfo.dwJoinTime?.toInt() ?: 0,
-        lastSpeakTimestamp = jceInfo.dwLastSpeakTime?.toInt() ?: 0
+        lastSpeakTimestamp = jceInfo.dwLastSpeakTime?.toInt() ?: 0,
+        isOfficialBot = client.groupConfig.isOfficialRobot(jceInfo.memberUin)
     )
 }

+ 4 - 0
mirai-core/src/commonMain/kotlin/network/QQAndroidBotNetworkHandler.kt

@@ -35,6 +35,7 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.MsgSvc
 import net.mamoe.mirai.internal.network.protocol.packet.*
 import net.mamoe.mirai.internal.network.protocol.packet.KnownPacketFactories.PacketFactoryIllegalStateException
 import net.mamoe.mirai.internal.network.protocol.packet.chat.GroupInfoImpl
+import net.mamoe.mirai.internal.network.protocol.packet.chat.TroopManagement
 import net.mamoe.mirai.internal.network.protocol.packet.chat.receive.MessageSvcPbGetMsg
 import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.internal.network.protocol.packet.login.ConfigPushSvc
@@ -386,6 +387,9 @@ internal class QQAndroidBotNetworkHandler(coroutineContext: CoroutineContext, bo
         if (initGroupOk) {
             return
         }
+        logger.info { "Start syncing group config..." }
+        TroopManagement.GetTroopConfig(bot.client).sendAndExpect<TroopManagement.GetTroopConfig.Response>()
+        logger.info { "Successfully synced group config." }
 
         logger.info { "Start loading group list..." }
         val troopListData = FriendList.GetTroopListSimplify(bot.client)

+ 12 - 0
mirai-core/src/commonMain/kotlin/network/QQAndroidClient.kt

@@ -207,6 +207,18 @@ internal open class QQAndroidClient(
      */
     val protocolVersion: Short = 8001
 
+    internal val groupConfig: GroupConfig = GroupConfig()
+
+    internal class GroupConfig {
+        var robotConfigVersion: Int = 0
+        var aioKeyWordVersion: Int = 0
+        var robotUinRangeList: List<LongRange> = emptyList()
+
+        fun isOfficialRobot(uin: Long): Boolean {
+            return robotUinRangeList.any { range -> range.contains(uin) }
+        }
+    }
+
     class MessageSvcSyncData {
         val firstNotify: AtomicBoolean = atomic(true)
 

+ 92 - 0
mirai-core/src/commonMain/kotlin/network/protocol/data/proto/OIDB.kt

@@ -102,6 +102,98 @@ internal class Oidb0x5d2 : ProtoBuf {
     ) : ProtoBuf
 }
 
+internal class Oidb0x496 {
+    @Serializable
+    internal class AioKeyword(
+        @JvmField @ProtoNumber(1) val keywords: List<AioKeywordInfo> = emptyList(),
+        @JvmField @ProtoNumber(2) val rules: List<AioKeywordRuleInfo> = emptyList(),
+        @JvmField @ProtoNumber(3) val version: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class AioKeywordInfo(
+        @JvmField @ProtoNumber(1) val word: String = "",
+        @JvmField @ProtoNumber(2) val ruleId: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class AioKeywordRuleInfo(
+        @JvmField @ProtoNumber(1) val ruleId: Int = 0,
+        @JvmField @ProtoNumber(2) val startTime: Int = 0,
+        @JvmField @ProtoNumber(3) val endTime: Int = 0,
+        @JvmField @ProtoNumber(4) val postionFlag: Int = 0,
+        @JvmField @ProtoNumber(5) val matchGroupClass: List<Int> = emptyList(),
+        @JvmField @ProtoNumber(6) val version: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class GroupMsgConfig(
+        @JvmField @ProtoNumber(1) val boolUinEnable: Boolean = false,
+        @JvmField @ProtoNumber(2) val maxAioMsg: Int = 0,
+        @JvmField @ProtoNumber(3) val enableHelper: Int = 0,
+        @JvmField @ProtoNumber(4) val groupMaxNumber: Int = 0,
+        @JvmField @ProtoNumber(5) val nextUpdateTime: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class MsgSeqInfo(
+        @JvmField @ProtoNumber(1) val groupCode: Long = 0L,
+        @JvmField @ProtoNumber(2) val managerUinList: List<Long> = emptyList(),
+        @JvmField @ProtoNumber(3) val updateTime: Long = 0L,
+        @JvmField @ProtoNumber(4) val firstUnreadManagerMsgSeq: Long = 0L,
+        @JvmField @ProtoNumber(5) val uint64ManagerMsgSeq: List<Long> = emptyList()
+    ) : ProtoBuf
+
+    @Serializable
+    internal class ReqBody(
+        @JvmField @ProtoNumber(1) val groupCode: Long = 0L,
+        @JvmField @ProtoNumber(2) val updateTime: Long = 0L,
+        @JvmField @ProtoNumber(3) val managerUinList: Long = 0L,
+        @JvmField @ProtoNumber(4) val firstUnreadManagerMsgSeq: Long = 0L,
+        @JvmField @ProtoNumber(5) val justFetchMsgConfig: Int = 0,
+        @JvmField @ProtoNumber(6) val type: Int = 0,
+        @JvmField @ProtoNumber(7) val version: Int = 0,
+        @JvmField @ProtoNumber(8) val aioKeywordVersion: Int = 0
+    ) : ProtoBuf
+
+    @Serializable
+    internal class Robot(
+        @JvmField @ProtoNumber(1) val version: Int = 0,
+        @JvmField @ProtoNumber(2) val uinRange: List<UinRange> = emptyList(),
+        @JvmField @ProtoNumber(3) val fireKeywords: List<String> = emptyList(),
+        @JvmField @ProtoNumber(4) val startKeywords: List<String> = emptyList(),
+        @JvmField @ProtoNumber(5) val endKeywords: List<String> = emptyList(),
+        @JvmField @ProtoNumber(6) val sessionTimeout: Int = 0,
+        @JvmField @ProtoNumber(7) val subscribeCategories: List<RobotSubscribeCategory> = emptyList()
+    ) : ProtoBuf
+
+    @Serializable
+    internal class RobotSubscribeCategory(
+        @JvmField @ProtoNumber(1) val id: Int = 0,
+        @JvmField @ProtoNumber(2) val name: String = "",
+        @JvmField @ProtoNumber(3) val type: Int = 0,
+        @JvmField @ProtoNumber(4) val nextWording: String = "",
+        @JvmField @ProtoNumber(5) val nextContent: String = ""
+    ) : ProtoBuf
+
+    @Serializable
+    internal class RspBody(
+        @JvmField @ProtoNumber(1) val msgSeqInfo: List<MsgSeqInfo> = emptyList(),
+        @JvmField @ProtoNumber(2) val maxAioMsg: Long = 0L,
+        @JvmField @ProtoNumber(3) val maxPositionMsg: Long = 0L,
+        @JvmField @ProtoNumber(4) val msgGroupMsgConfig: GroupMsgConfig? = null,
+        @JvmField @ProtoNumber(5) val robotConfig: Robot? = null,
+        @JvmField @ProtoNumber(6) val aioKeywordConfig: AioKeyword? = null
+    ) : ProtoBuf
+
+    @Serializable
+    internal class UinRange(
+        @JvmField @ProtoNumber(1) val startUin: Long = 0L,
+        @JvmField @ProtoNumber(2) val endUin: Long = 0L
+    ) : ProtoBuf
+}
+
+
 @Serializable
 internal class Oidb0x8a0 : ProtoBuf {
     @Serializable

+ 1 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/PacketFactory.kt

@@ -142,6 +142,7 @@ internal object KnownPacketFactories {
         TroopManagement.EditSpecialTitle,
         TroopManagement.Mute,
         TroopManagement.GroupOperation,
+        TroopManagement.GetTroopConfig,
         //  TroopManagement.GetGroupInfo,
         TroopManagement.EditGroupNametag,
         TroopManagement.Kick,

+ 44 - 0
mirai-core/src/commonMain/kotlin/network/protocol/packet/chat/TroopManagement.kt

@@ -147,6 +147,50 @@ internal class TroopManagement {
         }
     }
 
+    internal object GetTroopConfig : OutgoingPacketFactory<GetTroopConfig.Response>("OidbSvc.0x496") {
+        class Response(
+            val success: Boolean
+        ) : Packet {
+            override fun toString(): String = "TroopManagement.GetTroopConfig.Response($success)"
+        }
+
+        operator fun invoke(
+            client: QQAndroidClient
+        ): OutgoingPacket = buildOutgoingUniPacket(client) {
+            writeProtoBuf(
+                OidbSso.OIDBSSOPkg.serializer(), OidbSso.OIDBSSOPkg(
+                    command = 1174,
+                    result = 0,
+                    serviceType = 0,
+                    clientVersion = "android 8.4.18",
+                    bodybuffer = Oidb0x496.ReqBody(
+                        updateTime = 0,
+                        firstUnreadManagerMsgSeq = 1,
+                        version = client.groupConfig.robotConfigVersion,
+                        aioKeywordVersion = client.groupConfig.aioKeyWordVersion,
+                        type = 3
+                    ).toByteArray(Oidb0x496.ReqBody.serializer())
+                )
+            )
+        }
+
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+            readProtoBuf(OidbSso.OIDBSSOPkg.serializer()).let { pkg ->
+                pkg.bodybuffer.loadAs(Oidb0x496.RspBody.serializer()).let { data ->
+                    bot.client.groupConfig.let { config ->
+                        config.aioKeyWordVersion = data.aioKeywordConfig!!.version
+                        config.robotConfigVersion = data.robotConfig!!.version
+                        config.robotUinRangeList = data.robotConfig.uinRange.asSequence().map { range ->
+                            LongRange(range.startUin, range.endUin)
+                        }.toList()
+                    }
+                }
+
+                return Response(pkg.result == 0)
+            }
+        }
+    }
+
     internal object Kick : OutgoingPacketFactory<Kick.Response>("OidbSvc.0x8a0_0") {
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
             val ret = this.readBytes()

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

@@ -308,6 +308,87 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
     },
     // 传字符串信息
     0x10 to lambda732 { group: GroupImpl, bot: QQAndroidBot ->
+        discardExact(1)
+        readProtoBuf(TroopTips0x857.NotifyMsgBody.serializer()).let { body ->
+            when (body.optEnumType) {
+                1 -> body.optMsgGraytips?.let { tipsInfo ->
+                    val message = tipsInfo.optBytesContent.decodeToString()
+                    //机器人信息
+                    if (tipsInfo.robotGroupOpt != 0) {
+                        when (tipsInfo.robotGroupOpt) {
+                            //添加
+                            1 -> {
+                                val dataList = message.parseToMessageDataList()
+                                val invitor = dataList.first().let { messageData ->
+                                    group.getOrFail(messageData.data.toLong())
+                                }
+                                val member = dataList.last().let { messageData ->
+                                    group.newMember(
+                                        MemberInfoImpl(
+                                            uin = messageData.data.toLong(),
+                                            nick = messageData.text,
+                                            permission = MemberPermission.MEMBER,
+                                            remark = "",
+                                            nameCard = "",
+                                            specialTitle = "",
+                                            muteTimestamp = 0,
+                                            anonymousId = null,
+                                            isOfficialBot = true
+                                        )
+                                    ).cast<NormalMember>().also {
+                                        group.members.delegate.add(it)
+                                    }
+                                }
+                                return@lambda732 sequenceOf(MemberJoinEvent.Invite(member, invitor))
+                            }
+                            //移除
+                            2 -> {
+                                message.parseToMessageDataList().first().let {
+                                    val member = group.getOrFail(it.data.toLong())
+                                    group.members.delegate.remove(member)
+                                    return@lambda732 sequenceOf(MemberLeaveEvent.Quit(member))
+                                }
+                            }
+
+                            else -> {
+                                bot.network.logger.debug { "Unknown robotGroupOpt ${tipsInfo.robotGroupOpt}, message=$message" }
+                                return@lambda732 emptySequence()
+                            }
+                        }
+                    } else when {
+                        message.endsWith("群聊坦白说") -> {
+                            val new = when (message) {
+                                "管理员已关闭群聊坦白说" -> false
+                                "管理员已开启群聊坦白说" -> true
+                                else -> {
+                                    bot.network.logger.debug { "Unknown server confess talk messages $message" }
+                                    return@lambda732 emptySequence()
+                                }
+                            }
+                            return@lambda732 sequenceOf(
+                                GroupAllowConfessTalkEvent(
+                                    new,
+                                    !new,
+                                    group,
+                                    false
+                                )
+                            )
+                        }
+                        else -> {
+                            bot.network.logger.debug { "Unknown server messages $message" }
+                            return@lambda732 emptySequence()
+                        }
+                    }
+                }
+                else -> {
+                    bot.network.logger.debug {
+                        "Unknown Transformers732 0x10 optEnumType\noptEnumType=${body.optEnumType}\ncontent=${body._miraiContentToString()}"
+                    }
+                    return@lambda732 emptySequence()
+                }
+            } ?: return@lambda732 emptySequence()
+        }
+        /*
         val dataBytes = readBytes(26)
 
         when (dataBytes[0].toInt() and 0xFF) {
@@ -371,7 +452,7 @@ private object Transformers732 : Map<Int, Lambda732> by mapOf(
                     )
                 )*/
             }
-        }
+        }*/
     },
 
     // recall

+ 23 - 0
mirai-core/src/commonMain/kotlin/utils/string.kt

@@ -0,0 +1,23 @@
+package net.mamoe.mirai.internal.utils
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import net.mamoe.mirai.utils.MiraiInternalApi
+
+@Serializable
+internal data class MessageData(
+    val data: String,
+    val cmd: Int,
+    val text: String
+)
+
+@Suppress("RegExpRedundantEscape")
+internal val extraJsonPattern = Regex("<(\\{.*?\\})>")
+
+@MiraiInternalApi
+internal fun String.parseToMessageDataList(): Sequence<MessageData> {
+    return extraJsonPattern.findAll(this).filter { it.groups.size == 2 }.mapNotNull { result ->
+        Json.decodeFromString(MessageData.serializer(), result.groups[1]!!.value)
+    }
+}
+