Group: new design

This commit is contained in:
Him188 2019-11-28 23:11:24 +08:00
parent 0c49d8c923
commit b5efba2949
10 changed files with 135 additions and 69 deletions

View File

@ -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) }
}
}
}

View File

@ -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<Member> {
/**
* 内部 ID. 内部 ID [GroupId] 的映射
*/
val internalId: GroupInternalId
/**
* [Group] 实例创建的时候查询一次. 收到各事件后
* 群主 (同步事件更新)
* 进行 [updateGroupInfo] 时将会更新这个值.
*/
val member: ContactList<Member>
val owner: Member
/**
* 群名称 (同步事件更新)
* 进行 [updateGroupInfo] 时将会更新这个值.
*/
val name: String
/**
* 入群公告, 没有时为空字符串. (同步事件更新)
* 进行 [updateGroupInfo] 时将会更新这个值.
*/
val announcement: String
/**
* [Group] 实例创建的时候查询一次. 并与事件同步事件更新
*
* **注意**: 获得的列表仅为这一时刻的成员列表的镜像. 它将不会被更新
*/
val members: ContactList<Member>
/**
* 获取群成员. 若此 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]

View File

@ -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<Member> = MutableContactList()
override val member: ContactList<Member> = 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<Member> 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<RawGroupInfo>().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<Member> = 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)"
}

View File

@ -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")

View File

@ -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.

View File

@ -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),

View File

@ -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<Member>
) {
val group: Group get() = _group
val owner: Member get() = _owner
val name: String get() = _name
val announcement: String get() = _announcement
val members: ContactList<Member> 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<Member>
) : GroupPacket.GroupPacketResponse
/**
* 不含群主
*/
val members: Map<UInt, MemberPermission>
) : 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<GroupPacket.GroupPacketResponse>() {
@PacketVersion(date = "2019.10.19", timVersion = "2.3.2 (21173)")
fun Message(
@ -67,7 +96,7 @@ object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
* 退出群
*/
@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<GroupPacket.GroupPacketResponse>() {
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<GroupPacket.GroupPacketResponse>() {
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<GroupPacket.GroupPacketResponse>() {
val stop = readUInt() // 标记读取群成员的结束
discardExact(1) // 00
val members: MutableContactList<Member> = MutableContactList()
members[owner.id] = owner
val members = mutableMapOf<UInt, MemberPermission>()
do {
val qq = readUInt()
val status = readUShort().toInt() // 这个群成员的状态, 最后一 bit 为管理员权限. 这里面还包含其他状态
if (qq == owner.id) {
if (qq == owner) {
continue
}
@ -147,9 +175,9 @@ object GroupPacket : SessionPacketFactory<GroupPacket.GroupPacketResponse>() {
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
*

View File

@ -93,7 +93,8 @@ object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginResponse,
override fun toString(): String = "LoginResponse.CaptchaInit"
}
data class Success(
@Suppress("unused")
class Success(
val sessionResponseDecryptionKey: SessionResponseDecryptionKey,
val token38: IoBuffer,//56
@ -103,7 +104,9 @@ object SubmitPasswordPacket : PacketFactory<SubmitPasswordPacket.LoginResponse,
val nickname: String,
val age: Short,
val gender: Gender
) : LoginResponse()
) : LoginResponse() {
override fun toString(): String = "LoginResponse.Success"
}
data class Failed(val result: LoginResult) : LoginResponse()
}
@ -222,6 +225,7 @@ inline class SessionResponseDecryptionKey(private val delegate: IoBuffer) : Decr
override fun decrypt(input: ByteReadPacket): ByteReadPacket = input.decryptBy(delegate)
override fun toString(): String = "SessionResponseDecryptionKey"
companion object Type : DecrypterType<SessionResponseDecryptionKey>
}

View File

@ -59,10 +59,12 @@ object RequestSessionPacket : PacketFactory<RequestSessionPacket.SessionKeyRespo
}
}
data class SessionKeyResponse(
class SessionKeyResponse(
val sessionKey: SessionKey,
val tlv0105: ByteReadPacket? = null
) : Packet
) : Packet {
override fun toString(): String = "SessionKeyResponse"
}
override suspend fun ByteReadPacket.decode(id: PacketId, sequenceId: UShort, handler: BotNetworkHandler<*>): SessionKeyResponse {
when (remaining) {

View File

@ -46,33 +46,19 @@ object TouchPacket : PacketFactory<TouchPacket.TouchResponse, TouchKey>(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 {