Procházet zdrojové kódy

Merge remote-tracking branch 'origin/master'

jiahua.liu před 6 roky
rodič
revize
240183d5a5
20 změnil soubory, kde provedl 504 přidání a 193 odebrání
  1. 91 33
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt
  2. 61 5
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt
  3. 29 72
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt
  4. 15 14
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/OIDB.kt
  5. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt
  6. 27 20
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/TroopManagement.kt
  7. 9 0
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/MessageSvc.kt
  8. 52 30
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt
  9. 1 1
      mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendList.kt
  10. 6 0
      mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt
  11. 31 5
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt
  12. 10 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt
  13. 16 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/FriendInfo.kt
  14. 60 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/GroupInfo.kt
  15. 20 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt
  16. 4 0
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt
  17. 12 6
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt
  18. 1 2
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/At.kt
  19. 57 3
      mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/LockFreeLinkedList.kt
  20. 1 1
      mirai-demos/mirai-demo-gentleman/build.gradle

+ 91 - 33
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/ContactImpl.kt

@@ -11,9 +11,7 @@ package net.mamoe.mirai.qqandroid
 
 import kotlinx.coroutines.launch
 import net.mamoe.mirai.contact.*
-import net.mamoe.mirai.data.FriendNameRemark
-import net.mamoe.mirai.data.PreviousNameList
-import net.mamoe.mirai.data.Profile
+import net.mamoe.mirai.data.*
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.event.events.MessageSendEvent.FriendMessageSendEvent
@@ -24,6 +22,7 @@ import net.mamoe.mirai.message.data.MessageChain
 import net.mamoe.mirai.message.data.NotOnlineImageFromFile
 import net.mamoe.mirai.qqandroid.network.highway.HighwayHelper
 import net.mamoe.mirai.qqandroid.network.highway.postImage
+import net.mamoe.mirai.qqandroid.network.protocol.data.jce.StTroopMemberInfo
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Cmd0x352
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.image.ImgStore
@@ -33,6 +32,7 @@ import net.mamoe.mirai.qqandroid.utils.toIpV4AddressString
 import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.toUHexString
 import kotlin.coroutines.CoroutineContext
+import net.mamoe.mirai.qqandroid.network.protocol.data.jce.FriendInfo as JceFriendInfo
 
 internal abstract class ContactImpl : Contact {
     override fun hashCode(): Int {
@@ -49,10 +49,22 @@ internal abstract class ContactImpl : Contact {
     }
 }
 
-internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: CoroutineContext, override val id: Long) : ContactImpl(), QQ {
-    override val bot: QQAndroidBot by bot.unsafeWeakRef()
+internal inline class FriendInfoImpl(
+    private val jceFriendInfo: JceFriendInfo
+) : FriendInfo {
+    override val nick: String get() = jceFriendInfo.nick ?: ""
+    override val uin: Long get() = jceFriendInfo.friendUin
+}
 
-    override lateinit var nick: String
+internal class QQImpl(
+    bot: QQAndroidBot,
+    override val coroutineContext: CoroutineContext,
+    override val id: Long,
+    private val friendInfo: FriendInfo
+) : ContactImpl(), QQ {
+    override val bot: QQAndroidBot by bot.unsafeWeakRef()
+    override val nick: String
+        get() = friendInfo.nick
 
     override suspend fun sendMessage(message: MessageChain) {
         val event = FriendMessageSendEvent(this, message).broadcast()
@@ -135,14 +147,17 @@ internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: Coroutin
         image.input.close()
     }
 
+    @MiraiExperimentalAPI
     override suspend fun queryProfile(): Profile {
         TODO("not implemented")
     }
 
+    @MiraiExperimentalAPI
     override suspend fun queryPreviousNameList(): PreviousNameList {
         TODO("not implemented")
     }
 
+    @MiraiExperimentalAPI
     override suspend fun queryRemark(): FriendNameRemark {
         TODO("not implemented")
     }
@@ -156,24 +171,27 @@ internal class QQImpl(bot: QQAndroidBot, override val coroutineContext: Coroutin
 }
 
 
+@Suppress("MemberVisibilityCanBePrivate")
 internal class MemberImpl(
     qq: QQImpl,
-    var _groupCard: String,
-    var _specialTitle: String,
     group: GroupImpl,
     override val coroutineContext: CoroutineContext,
-    override var permission: MemberPermission
+    memberInfo: MemberInfo
 ) : ContactImpl(), Member, QQ by qq {
     override val group: GroupImpl by group.unsafeWeakRef()
     val qq: QQImpl by qq.unsafeWeakRef()
 
+    override var permission: MemberPermission = memberInfo.permission
+    internal var _nameCard: String = memberInfo.nameCard
+    internal var _specialTitle: String = memberInfo.specialTitle
+
     override var nameCard: String
-        get() = _groupCard
+        get() = _nameCard
         set(newValue) {
             group.checkBotPermissionOperator()
-            if (_groupCard != newValue) {
-                val oldValue = _groupCard
-                _groupCard = newValue
+            if (_nameCard != newValue) {
+                val oldValue = _nameCard
+                _nameCard = newValue
                 launch {
                     bot.network.run {
                         TroopManagement.EditGroupNametag(
@@ -223,7 +241,8 @@ internal class MemberImpl(
             ).sendAndExpect<TroopManagement.Mute.Response>()
         }
 
-        MemberMuteEvent(this@MemberImpl, durationSeconds, null).broadcast()
+        @Suppress("RemoveRedundantQualifierName") // or unresolved reference
+        net.mamoe.mirai.event.events.MemberMuteEvent(this@MemberImpl, durationSeconds, null).broadcast()
         return true
     }
 
@@ -241,7 +260,8 @@ internal class MemberImpl(
             ).sendAndExpect<TroopManagement.Mute.Response>()
         }
 
-        MemberUnmuteEvent(this@MemberImpl, null).broadcast()
+        @Suppress("RemoveRedundantQualifierName") // or unresolved reference
+        net.mamoe.mirai.event.events.MemberUnmuteEvent(this@MemberImpl, null).broadcast()
         return true
     }
 
@@ -269,25 +289,60 @@ internal class MemberImpl(
     override fun hashCode(): Int = super.hashCode()
 }
 
+internal class MemberInfoImpl(
+    private val jceInfo: StTroopMemberInfo,
+    private val groupOwnerId: Long
+) : MemberInfo {
+    override val uin: Long get() = jceInfo.memberUin
+    override val nameCard: String get() = jceInfo.sName ?: ""
+    override val nick: String get() = jceInfo.nick
+    override val permission: MemberPermission
+        get() = when {
+            jceInfo.memberUin == groupOwnerId -> MemberPermission.OWNER
+            jceInfo.dwFlag == 1L -> MemberPermission.ADMINISTRATOR
+            else -> MemberPermission.MEMBER
+        }
+    override val specialTitle: String get() = jceInfo.sSpecialTitle ?: ""
+}
 
 /**
  * 对GroupImpl
  * 中name/announcement的更改会直接向服务器异步汇报
  */
+@Suppress("PropertyName")
 @UseExperimental(MiraiInternalAPI::class)
 internal class GroupImpl(
     bot: QQAndroidBot, override val coroutineContext: CoroutineContext,
     override val id: Long,
-    val uin: Long,
-    var _name: String,
-    var _announcement: String,
-    var _allowMemberInvite: Boolean,
-    var _confessTalk: Boolean,
-    var _muteAll: Boolean,
-    var _autoApprove: Boolean,
-    var _anonymousChat: Boolean,
-    override val members: ContactList<Member>
+    groupInfo: GroupInfo,
+    members: Sequence<MemberInfo>
 ) : ContactImpl(), Group {
+    override val bot: QQAndroidBot by bot.unsafeWeakRef()
+    val uin: Long = groupInfo.uin
+
+    override lateinit var owner: Member
+
+    @UseExperimental(MiraiExperimentalAPI::class)
+    override lateinit var botPermission: MemberPermission
+
+    override val members: ContactList<Member> = ContactList(members.mapNotNull {
+        if (it.uin == bot.uin) {
+            botPermission = it.permission
+            null
+        } else Member(it).also { member ->
+            if (member.permission == MemberPermission.OWNER) {
+                owner = member
+            }
+        }
+    }.toLockFreeLinkedList())
+
+    internal var _name: String = groupInfo.name
+    internal var _announcement: String = groupInfo.memo
+    internal var _allowMemberInvite: Boolean = groupInfo.allowMemberInvite
+    internal var _confessTalk: Boolean = groupInfo.confessTalk
+    internal var _muteAll: Boolean = groupInfo.muteAll
+    internal var _autoApprove: Boolean = groupInfo.autoApprove
+    internal var _anonymousChat: Boolean = groupInfo.allowAnonymousChat
 
     override var name: String
         get() = _name
@@ -304,7 +359,7 @@ internal class GroupImpl(
                             newName = newValue
                         ).sendWithoutExpect()
                     }
-                    GroupNameChangeEvent(oldValue, newValue, this@GroupImpl, null).broadcast()
+                    GroupNameChangeEvent(oldValue, newValue, this@GroupImpl, true).broadcast()
                 }
             }
         }
@@ -377,7 +432,7 @@ internal class GroupImpl(
                             switch = newValue
                         ).sendWithoutExpect()
                     }
-                    GroupAllowConfessTalkEvent(oldValue, newValue, this@GroupImpl, null).broadcast()
+                    GroupAllowConfessTalkEvent(oldValue, newValue, this@GroupImpl, true).broadcast()
                 }
             }
         }
@@ -403,16 +458,21 @@ internal class GroupImpl(
             }
         }
 
-
-    override lateinit var owner: Member
-    @UseExperimental(MiraiExperimentalAPI::class)
-    override var botPermission: MemberPermission = MemberPermission.MEMBER
-
     override suspend fun quit(): Boolean {
         check(botPermission != MemberPermission.OWNER) { "An owner cannot quit from a owning group" }
         TODO("not implemented")
     }
 
+    @UseExperimental(MiraiExperimentalAPI::class)
+    override fun Member(memberInfo: MemberInfo): Member {
+        return MemberImpl(
+            bot.QQ(memberInfo) as QQImpl,
+            this,
+            this.coroutineContext,
+            memberInfo
+        )
+    }
+
 
     override operator fun get(id: Long): Member {
         return members.delegate.filteringGetOrNull { it.id == id } ?: throw NoSuchElementException("member $id not found in group $uin")
@@ -426,8 +486,6 @@ internal class GroupImpl(
         return members.delegate.filteringGetOrNull { it.id == id }
     }
 
-    override val bot: QQAndroidBot by bot.unsafeWeakRef()
-
     override suspend fun sendMessage(message: MessageChain) {
         val event = GroupMessageSendEvent(this, message).broadcast()
         if (event.isCancelled) {

+ 61 - 5
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/QQAndroidBot.kt

@@ -17,11 +17,18 @@ import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.QQ
 import net.mamoe.mirai.contact.filteringGetOrNull
 import net.mamoe.mirai.data.AddFriendResult
+import net.mamoe.mirai.data.FriendInfo
+import net.mamoe.mirai.data.GroupInfo
+import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.events.BotEvent
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.qqandroid.network.QQAndroidBotNetworkHandler
 import net.mamoe.mirai.qqandroid.network.QQAndroidClient
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
+import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.utils.*
+import kotlin.collections.asSequence
 import kotlin.coroutines.CoroutineContext
 
 @UseExperimental(MiraiInternalAPI::class)
@@ -31,22 +38,32 @@ internal expect class QQAndroidBot constructor(
     configuration: BotConfiguration
 ) : QQAndroidBotBase
 
-@UseExperimental(MiraiInternalAPI::class)
+@UseExperimental(MiraiInternalAPI::class, MiraiExperimentalAPI::class)
 internal abstract class QQAndroidBotBase constructor(
     context: Context,
     account: BotAccount,
     configuration: BotConfiguration
 ) : BotImpl<QQAndroidBotNetworkHandler>(account, configuration) {
     val client: QQAndroidClient =
-        QQAndroidClient(context, account, bot = @Suppress("LeakingThis") this as QQAndroidBot, device = configuration.deviceInfo?.invoke(context) ?: SystemDeviceInfo(context))
+        QQAndroidClient(
+            context,
+            account,
+            bot = @Suppress("LeakingThis") this as QQAndroidBot,
+            device = configuration.deviceInfo?.invoke(context) ?: SystemDeviceInfo(context)
+        )
     internal var firstLoginSucceed: Boolean = false
     override val uin: Long get() = client.uin
     override val qqs: ContactList<QQ> = ContactList(LockFreeLinkedList())
 
-    override val selfQQ: QQ by lazy { QQ(uin) }
+    override val selfQQ: QQ by lazy {
+        QQ(object : FriendInfo {
+            override val uin: Long get() = [email protected]
+            override val nick: String get() = [email protected]
+        })
+    }
 
-    override fun QQ(id: Long): QQ {
-        return QQImpl(this as QQAndroidBot, coroutineContext, id)
+    override fun QQ(friendInfo: FriendInfo): QQ {
+        return QQImpl(this as QQAndroidBot, coroutineContext, friendInfo.uin, friendInfo)
     }
 
     override fun createNetworkHandler(coroutineContext: CoroutineContext): QQAndroidBotNetworkHandler {
@@ -60,6 +77,45 @@ internal abstract class QQAndroidBotBase constructor(
         return groups.delegate.filteringGetOrNull { (it as GroupImpl).uin == uin } ?: throw NoSuchElementException("Can not found group with ID=${uin}")
     }
 
+    fun getGroupByUinOrNull(uin: Long): Group? {
+        return groups.delegate.filteringGetOrNull { (it as GroupImpl).uin == uin }
+    }
+
+    override suspend fun queryGroupList(): Sequence<Long> {
+        return network.run {
+            FriendList.GetTroopListSimplify(bot.client)
+                .sendAndExpect<FriendList.GetTroopListSimplify.Response>(retry = 2)
+        }.groups.asSequence().map { it.groupUin.shl(32) and it.groupCode }
+    }
+
+    override suspend fun queryGroupInfo(id: Long): GroupInfo = network.run {
+        TroopManagement.GetGroupInfo(
+            client = bot.client,
+            groupCode = id
+        ).sendAndExpect<GroupInfoImpl>()
+    }
+
+    override suspend fun queryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Sequence<MemberInfo> = network.run {
+        var nextUin = 0L
+        var sequence = sequenceOf<MemberInfoImpl>()
+        while (true) {
+            val data = FriendList.GetTroopMemberList(
+                client = bot.client,
+                targetGroupUin = groupUin,
+                targetGroupCode = groupCode,
+                nextUin = nextUin
+            ).sendAndExpect<FriendList.GetTroopMemberList.Response>(timeoutMillis = 3000)
+            sequence += data.members.asSequence().map { troopMemberInfo ->
+                MemberInfoImpl(troopMemberInfo, ownerId)
+            }
+            nextUin = data.nextUin
+            if (nextUin == 0L) {
+                break
+            }
+        }
+        return sequence
+    }
+
     override fun onEvent(event: BotEvent): Boolean {
         return firstLoginSucceed
     }

+ 29 - 72
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt

@@ -18,22 +18,19 @@ import kotlinx.io.core.ByteReadPacket
 import kotlinx.io.core.Input
 import kotlinx.io.core.buildPacket
 import kotlinx.io.core.use
-import net.mamoe.mirai.contact.ContactList
-import net.mamoe.mirai.contact.Member
-import net.mamoe.mirai.contact.MemberPermission
 import net.mamoe.mirai.data.MultiPacket
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.event.*
 import net.mamoe.mirai.event.events.BotOfflineEvent
 import net.mamoe.mirai.network.BotNetworkHandler
+import net.mamoe.mirai.qqandroid.FriendInfoImpl
 import net.mamoe.mirai.qqandroid.GroupImpl
-import net.mamoe.mirai.qqandroid.MemberImpl
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.QQImpl
 import net.mamoe.mirai.qqandroid.event.PacketReceivedEvent
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.MsgSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.*
-import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.TroopManagement
+import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.GroupInfoImpl
 import net.mamoe.mirai.qqandroid.network.protocol.packet.chat.receive.MessageSvc
 import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList
 import net.mamoe.mirai.qqandroid.network.protocol.packet.login.Heartbeat
@@ -148,7 +145,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                     totalFriendCount = data.totalFriendCount
                     data.friendList.forEach {
                         // atomic add
-                        bot.qqs.delegate.addLast(QQImpl(bot, bot.coroutineContext, it.friendUin)).also {
+                        bot.qqs.delegate.addLast(QQImpl(bot, bot.coroutineContext, it.friendUin, FriendInfoImpl(it))).also {
                             currentFriendCount++
                         }
                     }
@@ -171,32 +168,33 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
                     .sendAndExpect<FriendList.GetTroopListSimplify.Response>(retry = 2)
 
                 troopListData.groups.forEach { troopNum ->
-                    val contactList = ContactList(LockFreeLinkedList<Member>())
-                    val groupInfoResponse =
-                        TroopManagement.GetGroupOperationInfo(
-                            client = bot.client,
-                            groupCode = troopNum.groupCode
-                        ).sendAndExpect<TroopManagement.GetGroupOperationInfo.Response>()
-
-                    val group =
-                        GroupImpl(
-                            bot = bot,
-                            coroutineContext = bot.coroutineContext,
-                            id = troopNum.groupCode,
-                            uin = troopNum.groupUin,
-                            _name = troopNum.groupName,
-                            _announcement = troopNum.groupMemo,
-                            _allowMemberInvite = groupInfoResponse.allowMemberInvite,
-                            _confessTalk = groupInfoResponse.confessTalk,
-                            _muteAll = troopNum.dwShutUpTimestamp != 0L,
-                            _autoApprove = groupInfoResponse.autoApprove,
-                            _anonymousChat = groupInfoResponse.allowAnonymousChat,
-                            members = contactList
-                        )
-                    bot.groups.delegate.addLast(group)
                     launch {
                         try {
-                            fillTroopMemberList(group, contactList, troopNum.dwGroupOwnerUin)
+                            bot.groups.delegate.addLast(
+                                GroupImpl(
+                                    bot = bot,
+                                    coroutineContext = bot.coroutineContext,
+                                    id = troopNum.groupCode,
+                                    groupInfo = bot.queryGroupInfo(troopNum.groupCode).apply {
+                                        this as GroupInfoImpl
+
+                                        if (this.delegate.groupName == null) {
+                                            this.delegate.groupName = troopNum.groupName
+                                        }
+
+                                        if (this.delegate.groupMemo == null) {
+                                            this.delegate.groupMemo = troopNum.groupMemo
+                                        }
+
+                                        if (this.delegate.groupUin == null) {
+                                            this.delegate.groupUin = troopNum.groupUin
+                                        }
+
+                                        this.delegate.groupCode = troopNum.groupCode
+                                    },
+                                    members = bot.queryGroupMemberList(troopNum.groupUin, troopNum.groupCode, troopNum.dwGroupOwnerUin)
+                                )
+                            )
                         } catch (e: Exception) {
                             bot.logger.error("群${troopNum.groupCode}的列表拉取失败, 一段时间后将会重试")
                             bot.logger.error(e)
@@ -242,47 +240,6 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
         return lastException
     }
 
-    suspend fun fillTroopMemberList(group: GroupImpl, list: ContactList<Member>, owner: Long) {
-        bot.logger.verbose("开始获取群[${group.uin}]成员列表")
-        var size = 0
-        var nextUin = 0L
-        while (true) {
-            val data = FriendList.GetTroopMemberList(
-                client = bot.client,
-                targetGroupUin = group.uin,
-                targetGroupCode = group.id,
-                nextUin = nextUin
-            ).sendAndExpect<FriendList.GetTroopMemberList.Response>(timeoutMillis = 3000)
-            data.members.forEach { troopMemberInfo ->
-                val member = MemberImpl(
-                    qq = (bot.QQ(troopMemberInfo.memberUin) as QQImpl).also { it.nick = troopMemberInfo.nick },
-                    _groupCard = troopMemberInfo.sName ?: "",
-                    _specialTitle = troopMemberInfo.sSpecialTitle ?: "",
-                    group = group,
-                    coroutineContext = group.coroutineContext,
-                    permission = when {
-                        troopMemberInfo.memberUin == owner -> MemberPermission.OWNER
-                        troopMemberInfo.dwFlag == 1L -> MemberPermission.ADMINISTRATOR
-                        else -> MemberPermission.MEMBER
-                    }
-                )
-                if (member.permission == MemberPermission.OWNER) {
-                    group.owner = member
-                }
-                if (troopMemberInfo.memberUin != bot.uin) {
-                    list.delegate.addLast(member)
-                } else {
-                    group.botPermission = member.permission
-                }
-                size += data.members.size
-                nextUin = data.nextUin
-            }
-            if (nextUin == 0L) {
-                break
-            }
-        }
-    }
-
     /**
      * 缓存超时处理的 [Job]. 超时后将清空缓存, 以免阻碍后续包的处理
      */
@@ -360,7 +317,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler
             if (packet is CancellableEvent && packet.isCancelled) return
         }
 
-        bot.logger.info("Received packet: ${packet.toString().replace("\n", """\n""").replace("\r", "")}")
+        bot.logger.info("Received: ${packet.toString().replace("\n", """\n""").replace("\r", "")}")
 
         packetFactory?.run {
             when (this) {

+ 15 - 14
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/proto/OIDB.kt

@@ -180,32 +180,32 @@ class Oidb0x88d : ProtoBuf {
         @SerialId(12) val groupDefaultPage: Int? = null,
         @SerialId(13) val groupInfoSeq: Int? = null,
         @SerialId(14) val groupRoamingTime: Int? = null,
-        @SerialId(15) val ingGroupName: ByteArray? = null,
-        @SerialId(16) val ingGroupMemo: ByteArray? = null,
-        @SerialId(17) val ingGroupFingerMemo: ByteArray? = null,
-        @SerialId(18) val ingGroupClassText: ByteArray? = null,
+        @SerialId(15) var groupName: String? = null,
+        @SerialId(16) var groupMemo: String? = null,
+        @SerialId(17) val ingGroupFingerMemo: String? = null,
+        @SerialId(18) val ingGroupClassText: String? = null,
         @SerialId(19) val groupAllianceCode: List<Int>? = null,
         @SerialId(20) val groupExtraAdmNum: Int? = null,
-        @SerialId(21) val groupUin: Long? = null,
+        @SerialId(21) var groupUin: Long? = null,
         @SerialId(22) val groupCurMsgSeq: Int? = null,
         @SerialId(23) val groupLastMsgTime: Int? = null,
-        @SerialId(24) val ingGroupQuestion: ByteArray? = null,
-        @SerialId(25) val ingGroupAnswer: ByteArray? = null,
+        @SerialId(24) val ingGroupQuestion: String? = null,
+        @SerialId(25) val ingGroupAnswer: String? = null,
         @SerialId(26) val groupVisitorMaxNum: Int? = null,
         @SerialId(27) val groupVisitorCurNum: Int? = null,
         @SerialId(28) val levelNameSeq: Int? = null,
         @SerialId(29) val groupAdminMaxNum: Int? = null,
         @SerialId(30) val groupAioSkinTimestamp: Int? = null,
         @SerialId(31) val groupBoardSkinTimestamp: Int? = null,
-        @SerialId(32) val ingGroupAioSkinUrl: ByteArray? = null,
-        @SerialId(33) val ingGroupBoardSkinUrl: ByteArray? = null,
+        @SerialId(32) val ingGroupAioSkinUrl: String? = null,
+        @SerialId(33) val ingGroupBoardSkinUrl: String? = null,
         @SerialId(34) val groupCoverSkinTimestamp: Int? = null,
-        @SerialId(35) val ingGroupCoverSkinUrl: ByteArray? = null,
+        @SerialId(35) val ingGroupCoverSkinUrl: String? = null,
         @SerialId(36) val groupGrade: Int? = null,
         @SerialId(37) val activeMemberNum: Int? = null,
         @SerialId(38) val certificationType: Int? = null,
-        @SerialId(39) val ingCertificationText: ByteArray? = null,
-        @SerialId(40) val ingGroupRichFingerMemo: ByteArray? = null,
+        @SerialId(39) val ingCertificationText: String? = null,
+        @SerialId(40) val ingGroupRichFingerMemo: String? = null,
         @SerialId(41) val tagRecord: List<TagRecord>? = null,
         @SerialId(42) val groupGeoInfo: GroupGeoInfo? = null,
         @SerialId(43) val headPortraitSeq: Int? = null,
@@ -254,7 +254,7 @@ class Oidb0x88d : ProtoBuf {
         @SerialId(86) val isAllowConfGroupMemberNick: Int? = null,
         @SerialId(87) val isAllowConfGroupMemberAtAll: Int? = null,
         @SerialId(88) val isAllowConfGroupMemberModifyGroupName: Int? = null,
-        @SerialId(89) val ingLongGroupName: ByteArray? = null,
+        @SerialId(89) val longGroupName: String? = null,
         @SerialId(90) val cmduinJoinRealMsgSeq: Int? = null,
         @SerialId(91) val isGroupFreeze: Int? = null,
         @SerialId(92) val msgLimitFrequency: Int? = null,
@@ -265,7 +265,8 @@ class Oidb0x88d : ProtoBuf {
         @SerialId(97) val isAllowHlGuildBinary: Int? = null,
         @SerialId(98) val cmduinRingtoneId: Int? = null,
         @SerialId(99) val groupFlagext4: Int? = null,
-        @SerialId(100) val groupFreezeReason: Int? = null
+        @SerialId(100) val groupFreezeReason: Int? = null,
+        @SerialId(101) var groupCode: Long? = null // mirai 添加
     ) : ProtoBuf
 
     @Serializable

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

@@ -135,7 +135,7 @@ internal object KnownPacketFactories {
         TroopManagement.EditSpecialTitle,
         TroopManagement.Mute,
         TroopManagement.GroupOperation,
-        TroopManagement.GetGroupOperationInfo,
+        TroopManagement.GetGroupInfo,
         TroopManagement.EditGroupNametag,
         TroopManagement.Kick,
         Heartbeat.Alive

+ 27 - 20
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/TroopManagement.kt

@@ -14,6 +14,7 @@ import kotlinx.io.core.buildPacket
 import kotlinx.io.core.readBytes
 import kotlinx.io.core.toByteArray
 import kotlinx.serialization.toUtf8Bytes
+import net.mamoe.mirai.contact.Group
 import net.mamoe.mirai.contact.Member
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.qqandroid.QQAndroidBot
@@ -23,12 +24,27 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.jce.ModifyGroupCardReq
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.stUinInfo
 import net.mamoe.mirai.qqandroid.network.protocol.data.proto.*
-import net.mamoe.mirai.qqandroid.network.protocol.packet.EMPTY_BYTE_ARRAY
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket
 import net.mamoe.mirai.utils.daysToSeconds
 import net.mamoe.mirai.utils.io.encodeToString
+import net.mamoe.mirai.data.GroupInfo as MiraiGroupInfo
+
+internal inline class GroupInfoImpl(
+    internal val delegate: Oidb0x88d.GroupInfo
+) : MiraiGroupInfo, Packet {
+    override val uin: Long get() = delegate.groupUin ?: error("cannot find groupUin")
+    override val owner: Long get() = delegate.groupOwner ?: error("cannot find groupOwner")
+    override val groupCode: Long get() = Group.calculateGroupCodeByGroupUin(uin)
+    override val memo: String get() = delegate.groupMemo ?: error("cannot find groupMemo")
+    override val name: String get() = delegate.groupName ?: delegate.longGroupName ?: error("cannot find groupName")
+    override val allowMemberInvite get() = delegate.groupFlagExt?.and(0x000000c0) != 0
+    override val allowAnonymousChat get() = delegate.groupFlagExt?.and(0x40000000) == 0
+    override val autoApprove get() = delegate.groupFlagext3?.and(0x00100000) == 0
+    override val confessTalk get() = delegate.groupFlagext3?.and(0x00002000) == 0
+    override val muteAll: Boolean get() = delegate.shutupTimestamp != 0
+}
 
 internal class TroopManagement {
 
@@ -70,16 +86,7 @@ internal class TroopManagement {
     }
 
 
-    internal object GetGroupOperationInfo : OutgoingPacketFactory<GetGroupOperationInfo.Response>("OidbSvc.0x88d_7") {
-        class Response(
-            val allowAnonymousChat: Boolean,
-            val allowMemberInvite: Boolean,
-            val autoApprove: Boolean,
-            val confessTalk: Boolean
-        ) : Packet {
-            override fun toString(): String = "Response(GroupInfo)"
-        }
-
+    internal object GetGroupInfo : OutgoingPacketFactory<GroupInfoImpl>("OidbSvc.0x88d_7") {
         operator fun invoke(
             client: QQAndroidClient,
             groupCode: Long
@@ -109,8 +116,13 @@ internal class TroopManagement {
                                         cmduinUinFlag = 0,
                                         createSourceFlag = 0,
                                         noCodeFingerOpenFlag = 0,
-                                        ingGroupQuestion = EMPTY_BYTE_ARRAY,
-                                        ingGroupAnswer = EMPTY_BYTE_ARRAY
+                                        ingGroupQuestion = "",
+                                        ingGroupAnswer = "",
+                                        groupName = "",
+                                        longGroupName = "",
+                                        groupMemo = "",
+                                        groupUin = 0,
+                                        groupOwner = 0
                                     ),
                                     groupCode = groupCode
                                 )
@@ -121,14 +133,9 @@ internal class TroopManagement {
             }
         }
 
-        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
+        override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GroupInfoImpl {
             with(this.readBytes().loadAs(OidbSso.OIDBSSOPkg.serializer()).bodybuffer.loadAs(Oidb0x88d.RspBody.serializer()).stzrspgroupinfo!![0].stgroupinfo!!) {
-                return Response(
-                    allowMemberInvite = (this.groupFlagExt?.and(0x000000c0) != 0),
-                    allowAnonymousChat = (this.groupFlagExt?.and(0x40000000) == 0),
-                    autoApprove = (this.groupFlagext3?.and(0x00100000) == 0),
-                    confessTalk = (this.groupFlagext3?.and(0x00002000) == 0)
-                )
+                return GroupInfoImpl(this)
             }
         }
     }

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

@@ -133,6 +133,15 @@ internal class MessageSvc {
             val messages = resp.uinPairMsgs.asSequence().filterNot { it.msg == null }.flatMap { it.msg!!.asSequence() }.mapNotNull {
                 when (it.msgHead.msgType) {
                     33 -> {
+                        if (it.msgHead.authUin == bot.uin) {
+                            val group = bot.getGroupByUinOrNull(it.msgHead.fromUin)
+                            if (group == null) {
+                                TODO("查询群信息, 添加群")
+                            }
+                        }
+
+                        TODO("为 group 添加一个 fun Member() 来构造 member")
+                        // bot.getGroupByUin(it.msgHead.fromUin).members.delegate.addLast()
                         println("GroupUin" + it.msgHead.fromUin + "新群员" + it.msgHead.authUin + " 出现了[" + it.msgHead.authNick + "] 添加刷新")
                         null
                     }

+ 52 - 30
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/chat/receive/OnlinePush.kt

@@ -20,10 +20,10 @@ import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.event.broadcast
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.message.GroupMessage
+import net.mamoe.mirai.qqandroid.GroupImpl
 import net.mamoe.mirai.qqandroid.MemberImpl
 import net.mamoe.mirai.qqandroid.QQAndroidBot
 import net.mamoe.mirai.qqandroid.io.serialization.decodeUniPacket
-import net.mamoe.mirai.qqandroid.io.serialization.loadAs
 import net.mamoe.mirai.qqandroid.io.serialization.readProtoBuf
 import net.mamoe.mirai.qqandroid.message.toMessageChain
 import net.mamoe.mirai.qqandroid.network.protocol.data.jce.MsgInfo
@@ -34,7 +34,7 @@ import net.mamoe.mirai.qqandroid.network.protocol.data.proto.OnlinePushTrans
 import net.mamoe.mirai.qqandroid.network.protocol.packet.IncomingPacketFactory
 import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket
 import net.mamoe.mirai.qqandroid.network.protocol.packet.buildResponseUniPacket
-import net.mamoe.mirai.utils.cryptor.contentToString
+import net.mamoe.mirai.utils.MiraiInternalAPI
 import net.mamoe.mirai.utils.io.discardExact
 import net.mamoe.mirai.utils.io.read
 import net.mamoe.mirai.utils.io.readString
@@ -96,6 +96,7 @@ internal class OnlinePush {
 
     internal object PbPushTransMsg : IncomingPacketFactory<Packet>("OnlinePush.PbPushTransMsg", "OnlinePush.RespPush") {
 
+        @UseExperimental(MiraiInternalAPI::class)
         @ExperimentalUnsignedTypes
         override suspend fun ByteReadPacket.decode(bot: QQAndroidBot, sequenceId: Int): Packet {
             val content = this.readProtoBuf(OnlinePushTrans.PbMsgInfo.serializer())
@@ -109,17 +110,16 @@ internal class OnlinePush {
                         if (var4 != 0 && var4 != 1) {
                             var5 = readUInt().toLong()
                         }
+
+                        val group = bot.getGroupByUin(content.fromUin) as GroupImpl
                         if (var5 == 0L && this.remaining == 1L) {//管理员变更
-                            val groupUin = content.fromUin
+                            val newPermission = if (this.readByte().toInt() == 1) MemberPermission.ADMINISTRATOR else MemberPermission.MEMBER
 
-                            val member = bot.getGroupByUin(groupUin)[target] as MemberImpl
-                            val old = member.permission
-                            return if (this.readByte().toInt() == 1) {
-                                member.permission = MemberPermission.ADMINISTRATOR
-                                MemberPermissionChangeEvent(member, old, MemberPermission.ADMINISTRATOR)
+                            return if (target == bot.uin) {
+                                BotGroupPermissionChangeEvent(group, group.botPermission.also { group.botPermission = newPermission }, newPermission)
                             } else {
-                                member.permission = MemberPermission.MEMBER
-                                MemberPermissionChangeEvent(member, old, MemberPermission.ADMINISTRATOR)
+                                val member = group[target] as MemberImpl
+                                MemberPermissionChangeEvent(member, member.permission.also { member.permission = newPermission }, newPermission)
                             }
                         }
                     }
@@ -129,10 +129,12 @@ internal class OnlinePush {
                             val target = readUInt().toLong()
                             val groupUin = content.fromUin
 
-                            bot.getGroupByUin(groupUin).let {
-                                val member = it[target] as MemberImpl
+                            bot.getGroupByUin(groupUin).let { group ->
+                                val member = group[target] as MemberImpl
                                 this.discardExact(1)
-                                return MemberLeaveEvent.Kick(member, it.members[readUInt().toLong()])
+                                return MemberLeaveEvent.Kick(member, group.members[readUInt().toLong()].also {
+                                    group.members.delegate.remove(it)
+                                })
                             }
                         }
                     }
@@ -158,6 +160,7 @@ internal class OnlinePush {
                     when {
                         msgInfo.shMsgType.toInt() == 732 -> {
                             val group = bot.getGroup(this.readUInt().toLong())
+                            group as GroupImpl
 
                             when (val internalType = this.readShort().toInt()) {
                                 3073 -> { // mute
@@ -169,9 +172,19 @@ internal class OnlinePush {
 
                                     return if (target == 0L) {
                                         if (time == 0) {
-                                            GroupMuteAllEvent(origin = true, new = false, operator = operator, group = group)
+                                            GroupMuteAllEvent(
+                                                origin = group.muteAll.also { group._muteAll = false },
+                                                new = false,
+                                                operator = operator,
+                                                group = group
+                                            )
                                         } else {
-                                            GroupMuteAllEvent(origin = false, new = true, operator = operator, group = group)
+                                            GroupMuteAllEvent(
+                                                origin = group.muteAll.also { group._muteAll = true },
+                                                new = true,
+                                                operator = operator,
+                                                group = group
+                                            )
                                         }
                                     } else {
                                         val member = group[target]
@@ -184,9 +197,10 @@ internal class OnlinePush {
                                 }
                                 3585 -> { // 匿名
                                     val operator = group[this.readUInt().toLong()]
+                                    val switch = this.readInt() == 0
                                     return GroupAllowAnonymousChatEvent(
-                                        origin = group.anonymousChat,
-                                        new = this.readInt() == 0,
+                                        origin = group.anonymousChat.also { group._anonymousChat = switch },
+                                        new = switch,
                                         operator = operator,
                                         group = group
                                     )
@@ -196,23 +210,30 @@ internal class OnlinePush {
                                     val message = this.readString(this.readByte().toInt())
                                     println(dataBytes.toUHexString())
 
-                                    return if (dataBytes[0].toInt() != 59) {
-                                        return GroupNameChangeEvent(origin = group.name, new = message, group = group)
+                                    if (dataBytes[0].toInt() != 59) {
+                                        return GroupNameChangeEvent(
+                                            origin = group.name.also { group._name = message },
+                                            new = message,
+                                            group = group,
+                                            isByBot = false
+                                        )
                                     } else {
                                         //println(message + ":" + dataBytes.toUHexString())
                                         when (message) {
                                             "管理员已关闭群聊坦白说" -> {
                                                 return GroupAllowConfessTalkEvent(
-                                                    origin = group.confessTalk,
+                                                    origin = group.confessTalk.also { group._confessTalk = false },
                                                     new = false,
-                                                    group = group
+                                                    group = group,
+                                                    isByBot = false
                                                 )
                                             }
                                             "管理员已开启群聊坦白说" -> {
                                                 return GroupAllowConfessTalkEvent(
-                                                    origin = group.confessTalk,
-                                                    new = false,
-                                                    group = group
+                                                    origin = group.confessTalk.also { group._confessTalk = true },
+                                                    new = true,
+                                                    group = group,
+                                                    isByBot = false
                                                 )
                                             }
                                             else -> {
@@ -222,18 +243,19 @@ internal class OnlinePush {
                                         }
                                     }
                                 }
-                                4352 -> {
-                                    println(msgInfo.contentToString())
-                                    println(msgInfo.vMsg.toUHexString())
-                                }
+                                // 4352 -> {
+                                //     println(msgInfo.contentToString())
+                                //     println(msgInfo.vMsg.toUHexString())
+                                // }
                                 else -> {
                                     println("unknown group internal type $internalType , data: " + this.readBytes().toUHexString() + " ")
                                 }
                             }
                         }
                         msgInfo.shMsgType.toInt() == 528 -> {
-                            val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer())
-                            println(content.contentToString())
+                            println("unknown shtype ${msgInfo.shMsgType.toInt()}")
+                            // val content = msgInfo.vMsg.loadAs(OnlinePushPack.MsgType0x210.serializer())
+                            // println(content.contentToString())
                         }
                         else -> {
                             println("unknown shtype ${msgInfo.shMsgType.toInt()}")

+ 1 - 1
mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendList.kt

@@ -72,7 +72,7 @@ internal class FriendList {
             val members: List<StTroopMemberInfo>,
             val nextUin: Long
         ) : Packet {
-            override fun toString(): String = "Friendlist.GetTroopMemberList.Response"
+            override fun toString(): String = "FriendList.GetTroopMemberList.Response"
         }
 
     }

+ 6 - 0
mirai-core-qqandroid/src/jvmTest/kotlin/test/ProtoBufDataClassGenerator.kt

@@ -261,6 +261,12 @@ fun String.generateProtoBufDataClass(): GeneratedClass {
                     @Suppress("NAME_SHADOWING")
                     var name = _name
                     when {
+
+                        name.startsWith("string") -> {
+                            name = name.substringAfter("string").takeIf { it.isNotBlank() }?.adjustName() ?: "string"
+                            if (defaultValue == "EMPTY_BYTE_ARRAY")
+                                defaultValue = "\"\""
+                        }
                         name.startsWith("str") -> {
                             name = name.substringAfter("str").takeIf { it.isNotBlank() }?.adjustName() ?: "str"
                             if (defaultValue == "EMPTY_BYTE_ARRAY")

+ 31 - 5
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt

@@ -19,13 +19,13 @@ import kotlinx.io.core.IoBuffer
 import kotlinx.io.core.use
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.AddFriendResult
+import net.mamoe.mirai.data.FriendInfo
+import net.mamoe.mirai.data.GroupInfo
+import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.network.BotNetworkHandler
-import net.mamoe.mirai.utils.MiraiInternalAPI
-import net.mamoe.mirai.utils.MiraiLogger
-import net.mamoe.mirai.utils.WeakRef
+import net.mamoe.mirai.utils.*
 import net.mamoe.mirai.utils.io.transferTo
-import net.mamoe.mirai.utils.toList
 
 /**
  * 机器人对象. 一个机器人实例登录一个 QQ 账号.
@@ -65,6 +65,13 @@ abstract class Bot : CoroutineScope {
      */
     abstract val uin: Long
 
+    /**
+     * 昵称
+     */
+    @MiraiExperimentalAPI("还未支持")
+    val nick: String
+        get() = TODO("bot 昵称获取")
+
     /**
      * 日志记录器
      */
@@ -116,7 +123,7 @@ abstract class Bot : CoroutineScope {
      * [Bot] 无法管理这个对象, 但这个对象会以 [Bot] 的 [Job] 作为父 Job.
      * 因此, 当 [Bot] 被关闭后, 这个对象也会被关闭.
      */
-    abstract fun QQ(id: Long): QQ
+    abstract fun QQ(friendInfo: FriendInfo): QQ
 
     /**
      * 机器人加入的群列表.
@@ -131,6 +138,25 @@ abstract class Bot : CoroutineScope {
             ?: throw NoSuchElementException("No such group $id for bot ${this.uin}")
     }
 
+    /**
+     * 获取群列表. 返回值前 32 bits 为 uin, 后 32 bits 为 groupCode
+     */
+    abstract suspend fun queryGroupList(): Sequence<Long>
+
+    /**
+     * 查询群资料. 获得的仅为当前时刻的资料.
+     * 请优先使用 [getGroup] 然后查看群资料.
+     */
+    abstract suspend fun queryGroupInfo(id: Long): GroupInfo
+
+    /**
+     * 查询群成员列表.
+     * 请优先使用 [getGroup], [Group.members] 查看群成员.
+     *
+     * 这个函数很慢. 请不要频繁使用.
+     */
+    abstract suspend fun queryGroupMemberList(groupUin: Long, groupCode: Long, ownerId: Long): Sequence<MemberInfo>
+
     // TODO 目前还不能构造群对象. 这将在以后支持
 
     // endregion

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

@@ -12,8 +12,10 @@
 package net.mamoe.mirai.contact
 
 import kotlinx.coroutines.CoroutineScope
+import net.mamoe.mirai.data.MemberInfo
 import net.mamoe.mirai.event.events.*
 import net.mamoe.mirai.utils.MiraiExperimentalAPI
+import kotlin.jvm.JvmName
 
 /**
  * 群. 在 QQ Android 中叫做 "Troop"
@@ -117,6 +119,14 @@ interface Group : Contact, CoroutineScope {
      */
     suspend fun quit(): Boolean
 
+    /**
+     * 构造一个 [Member].
+     * 非特殊情况请不要使用这个函数. 优先使用 [get].
+     */
+    @MiraiExperimentalAPI("dangerous")
+    @Suppress("INAPPLICABLE_JVM_NAME")
+    @JvmName("newMember")
+    fun Member(memberInfo: MemberInfo): Member
 
     companion object {
 

+ 16 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/FriendInfo.kt

@@ -0,0 +1,16 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.data
+
+interface FriendInfo {
+    val uin: Long
+
+    val nick: String
+}

+ 60 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/GroupInfo.kt

@@ -0,0 +1,60 @@
+package net.mamoe.mirai.data
+
+import net.mamoe.mirai.Bot
+
+/**
+ * 群资料.
+ *
+ * 通过 [Bot.queryGroupInfo] 得到
+ */
+interface GroupInfo {
+    /**
+     * Uin
+     */
+    val uin: Long
+
+    /**
+     * 群号码
+     */ // 由 uin 计算得到
+    val groupCode: Long
+
+    /**
+     * 名称
+     */
+    val name: String // 不一定能获取到
+
+    /**
+     * 群主
+     */
+    val owner: Long // 不一定能获取到
+
+    /**
+     * 入群公告
+     */
+    val memo: String // 不一定能获取到
+
+    /**
+     * 允许群员邀请其他人加入群
+     */
+    val allowMemberInvite: Boolean
+
+    /**
+     * 允许匿名聊天
+     */
+    val allowAnonymousChat: Boolean
+
+    /**
+     * 自动审批加群请求
+     */
+    val autoApprove: Boolean
+
+    /**
+     * 坦白说开启状态
+     */
+    val confessTalk: Boolean
+
+    /**
+     * 全员禁言
+     */
+    val muteAll: Boolean
+}

+ 20 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/data/MemberInfo.kt

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 Mamoe Technologies and contributors.
+ *
+ * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证.
+ * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link.
+ *
+ * https://github.com/mamoe/mirai/blob/master/LICENSE
+ */
+
+package net.mamoe.mirai.data
+
+import net.mamoe.mirai.contact.MemberPermission
+
+interface MemberInfo : FriendInfo {
+    val nameCard: String
+
+    val permission: MemberPermission
+
+    val specialTitle: String
+}

+ 4 - 0
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/Subscribers.kt

@@ -74,6 +74,10 @@ interface Listener<in E : Event> : CompletableJob {
  * ```kotlin
  * bot.subscribe<Subscribe> { /* 一些处理 */ }
  * ```
+ *
+ * @see subscribeMessages 监听消息 DSL
+ * @see subscribeGroupMessages 监听群消息
+ * @see subscribeFriendMessages 监听好友消息
  */
 inline fun <reified E : Event> CoroutineScope.subscribe(crossinline handler: suspend E.(E) -> ListeningStatus): Listener<E> =
     E::class.subscribeInternal(Handler { it.handler(it) })

+ 12 - 6
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/event/events/BotEvents.kt

@@ -13,6 +13,7 @@ import net.mamoe.mirai.Bot
 import net.mamoe.mirai.contact.*
 import net.mamoe.mirai.data.Packet
 import net.mamoe.mirai.event.AbstractCancellableEvent
+import net.mamoe.mirai.event.BroadcastControllable
 import net.mamoe.mirai.event.CancellableEvent
 import net.mamoe.mirai.message.data.Image
 import net.mamoe.mirai.message.data.MessageChain
@@ -120,16 +121,19 @@ data class BotGroupPermissionChangeEvent(
     override val group: Group,
     val origin: MemberPermission,
     val new: MemberPermission
-) : BotPassiveEvent, GroupEvent
+) : BotPassiveEvent, GroupEvent, Packet
 
 // region 群设置
 
 /**
  * 群设置改变. 此事件广播前修改就已经完成.
  */
-interface GroupSettingChangeEvent<T> : GroupEvent, BotPassiveEvent {
+interface GroupSettingChangeEvent<T> : GroupEvent, BotPassiveEvent, BroadcastControllable {
     val origin: T
     val new: T
+
+    override val shouldBroadcast: Boolean
+        get() = origin != new
 }
 
 /**
@@ -138,7 +142,8 @@ interface GroupSettingChangeEvent<T> : GroupEvent, BotPassiveEvent {
 data class GroupNameChangeEvent(
     override val origin: String,
     override val new: String,
-    override val group: Group
+    override val group: Group,
+    val isByBot: Boolean
 ) : GroupSettingChangeEvent<String>, Packet
 
 /**
@@ -187,7 +192,8 @@ data class GroupAllowAnonymousChatEvent(
 data class GroupAllowConfessTalkEvent(
     override val origin: Boolean,
     override val new: Boolean,
-    override val group: Group
+    override val group: Group,
+    val isByBot: Boolean
 ) : GroupSettingChangeEvent<Boolean>, Packet
 
 /**
@@ -299,7 +305,7 @@ data class MemberPermissionChangeEvent(
 // region 禁言
 
 /**
- * 群成员被禁言事件. 操作人和被禁言的成员都不可能是机器人本人
+ * 群成员被禁言事件. 被禁言的成员都不可能是机器人本人
  */
 data class MemberMuteEvent(
     override val member: Member,
@@ -311,7 +317,7 @@ data class MemberMuteEvent(
 ) : GroupMemberEvent, Packet
 
 /**
- * 群成员被取消禁言事件. 操作人和被禁言的成员都不可能是机器人本人
+ * 群成员被取消禁言事件. 被禁言的成员都不可能是机器人本人
  */
 data class MemberUnmuteEvent(
     override val member: Member,

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

@@ -12,7 +12,6 @@
 package net.mamoe.mirai.message.data
 
 import net.mamoe.mirai.contact.Member
-import net.mamoe.mirai.contact.groupCardOrNick
 import net.mamoe.mirai.utils.MiraiInternalAPI
 
 
@@ -21,7 +20,7 @@ import net.mamoe.mirai.utils.MiraiInternalAPI
  */
 class At @MiraiInternalAPI constructor(val target: Long, val display: String) : Message {
     @UseExperimental(MiraiInternalAPI::class)
-    constructor(member: Member) : this(member.id, "@${member.groupCardOrNick}")
+    constructor(member: Member) : this(member.id, "@${member.nick}")
 
     override fun toString(): String = display
 

+ 57 - 3
mirai-core/src/commonMain/kotlin/net.mamoe.mirai/utils/LockFreeLinkedList.kt

@@ -68,6 +68,20 @@ fun <E> LockFreeLinkedList<E>.asSequence(): Sequence<E> {
     }
 }
 
+/**
+ * 构建链表结构然后转为 [LockFreeLinkedList]
+ */
+fun <E> Iterable<E>.toLockFreeLinkedList(): LockFreeLinkedList<E> {
+    return LockFreeLinkedList<E>().apply { addAll(this@toLockFreeLinkedList) }
+}
+
+/**
+ * 构建链表结构然后转为 [LockFreeLinkedList]
+ */
+fun <E> Sequence<E>.toLockFreeLinkedList(): LockFreeLinkedList<E> {
+    return LockFreeLinkedList<E>().apply { addAll(this@toLockFreeLinkedList) }
+}
+
 /**
  * Implementation of lock-free LinkedList.
  *
@@ -111,8 +125,10 @@ open class LockFreeLinkedList<E> {
     }
 
     open fun addLast(element: E) {
-        val node = element.asNode(tail)
+        addLastNode(element.asNode(tail))
+    }
 
+    private fun addLastNode(node: Node<E>) {
         while (true) {
             val tail = head.iterateBeforeFirst { it === tail } // find the last node.
             if (tail.nextNodeRef.compareAndSet(this.tail, node)) { // ensure the last node is the last node
@@ -121,6 +137,46 @@ open class LockFreeLinkedList<E> {
         }
     }
 
+    /**
+     * 先把元素建立好链表, 再加入到 list.
+     */
+    @Suppress("DuplicatedCode")
+    open fun addAll(iterable: Iterable<E>) {
+        var firstNode: Node<E>? = null
+
+        var currentNode: Node<E>? = null
+        iterable.forEach {
+            val nextNode = it.asNode(tail)
+            if (firstNode == null) {
+                firstNode = nextNode
+            }
+            currentNode?.nextNode = nextNode
+            currentNode = nextNode
+        }
+
+        firstNode?.let { addLastNode(it) }
+    }
+
+    /**
+     * 先把元素建立好链表, 再加入到 list.
+     */
+    @Suppress("DuplicatedCode")
+    open fun addAll(iterable: Sequence<E>) {
+        var firstNode: Node<E>? = null
+
+        var currentNode: Node<E>? = null
+        iterable.forEach {
+            val nextNode = it.asNode(tail)
+            if (firstNode == null) {
+                firstNode = nextNode
+            }
+            currentNode?.nextNode = nextNode
+            currentNode = nextNode
+        }
+
+        firstNode?.let { addLastNode(it) }
+    }
+
     open operator fun plusAssign(element: E) = this.addLast(element)
 
     /**
@@ -243,8 +299,6 @@ open class LockFreeLinkedList<E> {
         }
     }
 
-    open fun addAll(elements: Collection<E>) = elements.forEach { addLast(it) }
-
     @Suppress("unused")
     open fun clear() {
         val first = head.nextNode

+ 1 - 1
mirai-demos/mirai-demo-gentleman/build.gradle

@@ -17,7 +17,7 @@ dependencies {
     implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion")
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-io:$coroutinesIoVersion")
     implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.62'
-    implementation 'org.jsoup:jsoup:1.12.1'
+    api 'org.jsoup:jsoup:1.12.1'
 }
 
 run{