From b5efba294914c40198baaad92d9b33e905aa9dd8 Mon Sep 17 00:00:00 2001 From: Him188 Date: Thu, 28 Nov 2019 23:11:24 +0800 Subject: [PATCH] Group: new design --- .../commonMain/kotlin/net.mamoe.mirai/Bot.kt | 4 +- .../kotlin/net.mamoe.mirai/contact/Group.kt | 50 +++++++++++--- .../contact/internal/ContactImpl.kt | 36 +++++++--- .../kotlin/net.mamoe.mirai/message/Image.kt | 4 +- .../net.mamoe.mirai/network/BotSession.kt | 2 +- .../network/protocol/tim/packet/PacketId.kt | 2 +- .../protocol/tim/packet/action/GroupPacket.kt | 66 +++++++++++++------ .../tim/packet/login/PasswordSubmission.kt | 8 ++- .../protocol/tim/packet/login/Session.kt | 6 +- .../protocol/tim/packet/login/Touch.kt | 26 ++------ 10 files changed, 135 insertions(+), 69 deletions(-) diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt index 780d19bb7..0676621cc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/Bot.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import net.mamoe.mirai.Bot.ContactSystem import net.mamoe.mirai.contact.* -import net.mamoe.mirai.contact.internal.GroupImpl +import net.mamoe.mirai.contact.internal.Group import net.mamoe.mirai.contact.internal.QQImpl import net.mamoe.mirai.network.BotNetworkHandler import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler @@ -139,7 +139,7 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope { suspend fun getGroup(id: GroupId): Group = id.value.let { if (_groups.containsKey(it)) _groups[it]!! else groupsLock.withLock { - _groups.getOrPut(it) { GroupImpl(bot, id) } + _groups.getOrPut(it) { Group(bot, id) } } } } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt index c431b3cb7..b115d23bc 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/Group.kt @@ -3,7 +3,7 @@ package net.mamoe.mirai.contact import net.mamoe.mirai.network.protocol.tim.packet.action.GroupInfo -import net.mamoe.mirai.network.protocol.tim.packet.action.QuiteGroupResponse +import net.mamoe.mirai.network.protocol.tim.packet.action.QuitGroupResponse import net.mamoe.mirai.utils.internal.PositiveNumbers import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail @@ -16,35 +16,65 @@ import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail * - Group ID([Group.internalId]) 是与调用 API 时使用的 id.(在 QQ 客户端中不可见) * @author Him188moe */ -interface Group : Contact { +interface Group : Contact, Iterable { + /** + * 内部 ID. 内部 ID 为 [GroupId] 的映射 + */ val internalId: GroupInternalId /** - * 在 [Group] 实例创建的时候查询一次. 收到各事件后 + * 群主 (同步事件更新) + * 进行 [updateGroupInfo] 时将会更新这个值. */ - val member: ContactList + val owner: Member + + /** + * 群名称 (同步事件更新) + * 进行 [updateGroupInfo] 时将会更新这个值. + */ + val name: String + + /** + * 入群公告, 没有时为空字符串. (同步事件更新) + * 进行 [updateGroupInfo] 时将会更新这个值. + */ + val announcement: String + + /** + * 在 [Group] 实例创建的时候查询一次. 并与事件同步事件更新 + * + * **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新 + */ + val members: ContactList + /** + * 获取群成员. 若此 ID 的成员不存在, 则会抛出 [kotlin.NoSuchElementException] + */ suspend fun getMember(id: UInt): Member /** - * 查询群资料 - */ // should be `suspend val` if kotlin supports in the future - suspend fun queryGroupInfo(): GroupInfo + * 更新群资料. 群资料会与服务器事件同步事件更新, 一般情况下不需要手动更新. + * + * @return 这一时刻的群资料 + */ + suspend fun updateGroupInfo(): GroupInfo /** * 让机器人退出这个群. 机器人必须为非群主才能退出. 否则将会失败 * - * @see QuiteGroupResponse.isSuccess 判断是否成功 + * @see QuitGroupResponse.isSuccess 判断是否成功 */ - suspend fun quite(): QuiteGroupResponse + suspend fun quit(): QuitGroupResponse + + fun toFullString(): String = "Group(id=${this.id}, name=$name, owner=${owner.id}, members=${members.idContentString})" } /** * 一般的用户可见的 ID. * 在 TIM/QQ 客户端中所看到的的号码均是这个 ID. * - * 注: 在引用群 ID 时, 应使用 [GroupId] 或 [GroupInternalId] 类型, 而不是 [UInt] + * 注: 在引用群 ID 时, 只应使用 [GroupId] 或 [GroupInternalId] 类型 (内联类无性能损失), 而不能使用 [UInt]. * * @see GroupInternalId.toId 由 [GroupInternalId] 转换为 [GroupId] * @see GroupId.toInternalId 由 [GroupId] 转换为 [GroupInternalId] diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/internal/ContactImpl.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/internal/ContactImpl.kt index 0088813ed..3a4ed8f59 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/internal/ContactImpl.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/contact/internal/ContactImpl.kt @@ -2,7 +2,6 @@ package net.mamoe.mirai.contact.internal -import kotlinx.coroutines.sync.Mutex import net.mamoe.mirai.Bot import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.data.Profile @@ -26,32 +25,50 @@ internal sealed class ContactImpl : Contact { override suspend fun sendMessage(message: Message) = sendMessage(message.chain()) } +/** + * 构造 [Group] + */ +@Suppress("FunctionName") +@PublishedApi +internal suspend fun Group(bot: Bot, groupId: GroupId): Group { + val info: RawGroupInfo = try { + bot.withSession { GroupPacket.QueryGroupInfo(qqAccount, groupId.toInternalId(), sessionKey).sendAndExpect() } + } catch (e: Exception) { + error("Cannot obtain group info for id ${groupId.value}") + } + return GroupImpl(bot, groupId).apply { this.info = info.parseBy(this) } +} + @Suppress("MemberVisibilityCanBePrivate", "CanBeParameter") internal data class GroupImpl internal constructor(override val bot: Bot, val groupId: GroupId) : ContactImpl(), Group { override val id: UInt get() = groupId.value override val internalId = GroupId(id).toInternalId() - private val _members: MutableContactList = MutableContactList() - override val member: ContactList = ContactList(_members) - private val membersLock: Mutex = Mutex() + internal lateinit var info: GroupInfo + override val owner: Member get() = info.owner + override val name: String get() = info.name + override val announcement: String get() = info.announcement + override val members: ContactList get() = info.members override suspend fun getMember(id: UInt): Member = - if (_members.containsKey(id)) _members[id]!! + if (members.containsKey(id)) members[id]!! else throw NoSuchElementException("No such member whose id is $id in group $id") override suspend fun sendMessage(message: MessageChain) { bot.sendPacket(GroupPacket.Message(bot.qqAccount, internalId, bot.sessionKey, message)) } - override suspend fun queryGroupInfo(): GroupInfo = bot.withSession { - GroupPacket.QueryGroupInfo(qqAccount, internalId, sessionKey).sendAndExpect() + override suspend fun updateGroupInfo(): GroupInfo = bot.withSession { + GroupPacket.QueryGroupInfo(qqAccount, internalId, sessionKey).sendAndExpect().parseBy(this@GroupImpl) } - override suspend fun quite(): QuiteGroupResponse = bot.withSession { - GroupPacket.QuiteGroup(qqAccount, sessionKey, internalId).sendAndExpect() + override suspend fun quit(): QuitGroupResponse = bot.withSession { + GroupPacket.QuitGroup(qqAccount, sessionKey, internalId).sendAndExpect() } override fun toString(): String = "Group(${this.id})" + + override fun iterator(): Iterator = members.delegate.values.iterator() } internal data class QQImpl internal constructor(override val bot: Bot, override val id: UInt) : ContactImpl(), QQ { @@ -76,6 +93,7 @@ internal data class QQImpl internal constructor(override val bot: Bot, override /** * 群成员 */ +@PublishedApi internal data class MemberImpl(private val delegate: QQ, override val group: Group, override val permission: MemberPermission) : QQ by delegate, Member { override fun toString(): String = "Member(id=${this.id}, permission=$permission)" } \ No newline at end of file diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Image.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Image.kt index 680d2d875..e237802db 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Image.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/Image.kt @@ -5,8 +5,6 @@ package net.mamoe.mirai.message import net.mamoe.mirai.contact.Contact import net.mamoe.mirai.network.protocol.tim.packet.action.FriendImagePacket import net.mamoe.mirai.utils.ExternalImage -import net.mamoe.mirai.utils.io.debugPrintln -import net.mamoe.mirai.utils.io.toUHexString /** @@ -40,7 +38,7 @@ class ImageId0x03 constructor(override inline val value: String, inline val uniq .replace("-", "") .chunked(2) .map { (it[0] + it[1].toString()).toUByte(16).toByte() } - .toByteArray().also { check(it.size == 16); debugPrintln("image md5=" + it.toUHexString()); debugPrintln("imageId=$this") } + .toByteArray().also { check(it.size == 16) } } @Suppress("FunctionName", "NOTHING_TO_INLINE") diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt index 53e685e39..6ee04100d 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/BotSession.kt @@ -161,7 +161,7 @@ abstract class BotSessionBase( inline val BotSession.isOpen: Boolean get() = socket.isOpen -inline val BotSession.qqAccount: UInt get() = bot.account.id +inline val BotSession.qqAccount: UInt get() = bot.account.id // 为了与群和好友的 id 区别. /** * 取得 [BotNetworkHandler] 的 sessionKey. diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/PacketId.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/PacketId.kt index 5cc48a7fc..496e8bc3e 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/PacketId.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/PacketId.kt @@ -69,7 +69,7 @@ enum class KnownPacketId(override inline val value: UShort, override inline val inline HEARTBEAT(0x0058u, HeartbeatPacket), inline S_KEY(0x001Du, RequestSKeyPacket), inline ACCOUNT_INFO(0x005Cu, RequestAccountInfoPacket), - inline SEND_GROUP_MESSAGE(0x0002u, GroupPacket), + inline GROUP_PACKET(0x0002u, GroupPacket), inline SEND_FRIEND_MESSAGE(0x00CDu, SendFriendMessagePacket), inline CAN_ADD_FRIEND(0x00A7u, CanAddFriendPacket), inline ADD_FRIEND(0x00A8u, AddFriendPacket), diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupPacket.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupPacket.kt index 8aefd76fa..4765bf5f9 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupPacket.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/action/GroupPacket.kt @@ -5,7 +5,6 @@ package net.mamoe.mirai.network.protocol.tim.packet.action import kotlinx.io.core.* import net.mamoe.mirai.contact.* import net.mamoe.mirai.contact.internal.MemberImpl -import net.mamoe.mirai.getQQ import net.mamoe.mirai.message.MessageChain import net.mamoe.mirai.message.internal.toPacket import net.mamoe.mirai.network.BotNetworkHandler @@ -18,18 +17,48 @@ import net.mamoe.mirai.withSession /** * 群资料 */ -data class GroupInfo( - val group: Group, - val owner: Member, +@Suppress("MemberVisibilityCanBePrivate") // 将来使用 +class GroupInfo( + internal var _group: Group, + internal var _owner: Member, + internal var _name: String, + internal var _announcement: String, + internal var _members: ContactList +) { + val group: Group get() = _group + val owner: Member get() = _owner + val name: String get() = _name + val announcement: String get() = _announcement + val members: ContactList get() = _members + + override fun toString(): String = "GroupInfo(id=${group.id}, owner=$owner, name=$name, announcement=$announcement, members=${members.idContentString}" +} + +data class RawGroupInfo( + val group: UInt, + val owner: UInt, val name: String, val announcement: String, - val members: ContactList -) : GroupPacket.GroupPacketResponse + /** + * 不含群主 + */ + val members: Map +) : GroupPacket.GroupPacketResponse { + suspend inline fun parseBy(group: Group): GroupInfo = group.bot.withSession { + GroupInfo( + group, + MemberImpl(this@RawGroupInfo.owner.qq(), group, MemberPermission.OWNER), + this@RawGroupInfo.name, + this@RawGroupInfo.announcement, + ContactList(this@RawGroupInfo.members.mapValuesTo(MutableContactList()) { MemberImpl(it.key.qq(), group, MemberPermission.OWNER) }) + ) + } +} /** * 退出群的返回 */ -inline class QuiteGroupResponse(private val _group: GroupInternalId?) : Packet, GroupPacket.GroupPacketResponse { +inline class QuitGroupResponse(private val _group: GroupInternalId?) : Packet, GroupPacket.GroupPacketResponse { val group: GroupInternalId get() = _group ?: error("request failed") val isSuccess: Boolean get() = _group != null @@ -37,7 +66,7 @@ inline class QuiteGroupResponse(private val _group: GroupInternalId?) : Packet, } @Suppress("FunctionName") -@AnnotatedId(KnownPacketId.SEND_GROUP_MESSAGE) +@AnnotatedId(KnownPacketId.GROUP_PACKET) object GroupPacket : SessionPacketFactory() { @PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)") fun Message( @@ -67,7 +96,7 @@ object GroupPacket : SessionPacketFactory() { * 退出群 */ @PacketVersion(date = "2019.11.28", timVersion = "2.3.2 (21173)") - fun QuiteGroup( + fun QuitGroup( bot: UInt, sessionKey: SessionKey, group: GroupInternalId @@ -84,7 +113,7 @@ object GroupPacket : SessionPacketFactory() { bot: UInt, groupInternalId: GroupInternalId, sessionKey: SessionKey - ): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "QueryBulletin", headerSizeHint = 9) { + ): OutgoingPacket = buildSessionPacket(bot, sessionKey, name = "QueryGroupInfo", headerSizeHint = 9) { writeUByte(0x72u) writeGroup(groupInternalId) writeZero(4) @@ -105,19 +134,19 @@ object GroupPacket : SessionPacketFactory() { 0x09u -> { if (readByte().toInt() == 0) { - QuiteGroupResponse(readUInt().groupInternalId()) + QuitGroupResponse(readUInt().groupInternalId()) } else { - QuiteGroupResponse(null) + QuitGroupResponse(null) } } 0x72u -> { discardExact(1) // 00 discardExact(4) // group internal id - val group = readUInt().group() // group id + val group = readUInt() // group id discardExact(13) //00 00 00 03 01 01 00 04 01 00 80 01 40 - val owner = MemberImpl(readUInt().qq(), group, MemberPermission.OWNER) + val owner = readUInt() discardExact(22) val groupName = readUByteLVString() @@ -134,12 +163,11 @@ object GroupPacket : SessionPacketFactory() { val stop = readUInt() // 标记读取群成员的结束 discardExact(1) // 00 - val members: MutableContactList = MutableContactList() - members[owner.id] = owner + val members = mutableMapOf() do { val qq = readUInt() val status = readUShort().toInt() // 这个群成员的状态, 最后一 bit 为管理员权限. 这里面还包含其他状态 - if (qq == owner.id) { + if (qq == owner) { continue } @@ -147,9 +175,9 @@ object GroupPacket : SessionPacketFactory() { 1 -> MemberPermission.OPERATOR else -> MemberPermission.MEMBER } - members[qq] = MemberImpl(handler.bot.getQQ(qq), group, permission) + members[qq] = permission } while (qq != stop && remaining != 0L) - return GroupInfo(group, owner, groupName, announcement, ContactList(members)) + return RawGroupInfo(group, owner, groupName, announcement, members) /* * 群 Mirai * diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/PasswordSubmission.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/PasswordSubmission.kt index 2c491bea5..1c8f5a3fd 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/PasswordSubmission.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/PasswordSubmission.kt @@ -93,7 +93,8 @@ object SubmitPasswordPacket : PacketFactory } diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Session.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Session.kt index 5c98eb1dc..07d80abcd 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Session.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Session.kt @@ -59,10 +59,12 @@ object RequestSessionPacket : PacketFactory): SessionKeyResponse { when (remaining) { diff --git a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Touch.kt b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Touch.kt index edd557905..2c7283754 100644 --- a/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Touch.kt +++ b/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/network/protocol/tim/packet/login/Touch.kt @@ -46,33 +46,19 @@ object TouchPacket : PacketFactory(TouchKey } sealed class TouchResponse : Packet { - data class OK( + class OK( var loginTime: Int, val loginIP: String, val token0825: ByteArray ) : TouchResponse() { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is OK) return false - - if (loginTime != other.loginTime) return false - if (loginIP != other.loginIP) return false - if (!token0825.contentEquals(other.token0825)) return false - - return true - } - - override fun hashCode(): Int { - var result = loginTime - result = 31 * result + loginIP.hashCode() - result = 31 * result + token0825.contentHashCode() - return result - } + override fun toString(): String = "TouchResponse.OK" } - data class Redirection( + class Redirection( val serverIP: String? = null - ) : TouchResponse() + ) : TouchResponse() { + override fun toString(): String = "TouchResponse.Redirection" + } } override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): TouchResponse {