Redesign Contacts: Use interfaces and hide internal implementations

This commit is contained in:
Him188 2019-11-24 17:29:05 +08:00
parent 1a56235cb0
commit b797ef3cc1
14 changed files with 327 additions and 210 deletions

View File

@ -7,6 +7,8 @@ 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.QQImpl
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMBotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.login.LoginResult
@ -95,27 +97,31 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
* @see Bot.contacts
*/
inner class ContactSystem internal constructor() {
inline val bot: Bot get() = this@Bot
val bot: Bot get() = this@Bot
private val _groups = ContactList<Group>()
private lateinit var groupsUpdater: Job
val groups = ContactList<Group>()
@Suppress("PropertyName")
internal val _groups = MutableContactList<Group>()
internal lateinit var groupsUpdater: Job
private val groupsLock = Mutex()
private val _qqs = ContactList<QQ>() //todo 实现群列表和好友列表获取
private lateinit var qqUpdaterJob: Job
val qqs: ContactList<QQ> = _qqs
val groups: ContactList<Group> = ContactList(_groups)
@Suppress("PropertyName")
internal val _qqs = MutableContactList<QQ>() //todo 实现群列表和好友列表获取
internal lateinit var qqUpdaterJob: Job
private val qqsLock = Mutex()
val qqs: ContactList<QQ> = ContactList(_qqs)
/**
* 获取缓存的 QQ 对象. 若没有对应的缓存, 则会创建一个.
*
* : 这个方法是线程安全的
*/
suspend fun getQQ(id: UInt): QQ =
if (qqs.containsKey(id)) qqs[id]!!
if (_qqs.containsKey(id)) _qqs[id]!!
else qqsLock.withLock {
qqs.getOrPut(id) { QQ(bot, id) }
_qqs.getOrPut(id) { QQImpl(bot, id) }
}
/**
@ -131,12 +137,11 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
* : 这个方法是线程安全的
*/
suspend fun getGroup(id: GroupId): Group = id.value.let {
if (groups.containsKey(it)) groups[it]!!
if (_groups.containsKey(it)) _groups[it]!!
else groupsLock.withLock {
groups.getOrPut(it) { Group(bot, id) }
_groups.getOrPut(it) { GroupImpl(bot, id) }
}
}
}
suspend inline fun Int.qq(): QQ = getQQ(this.coerceAtLeastOrFail(0).toUInt())
@ -152,8 +157,8 @@ class Bot(val account: BotAccount, val logger: MiraiLogger) : CoroutineScope {
suspend fun close() {
network.close()
this.coroutineContext.cancelChildren()
contacts.groups.clear()
contacts.qqs.clear()
contacts._groups.clear()
contacts._qqs.clear()
}
companion object {

View File

@ -38,14 +38,12 @@ suspend inline fun Bot.getGroup(internalId: GroupInternalId): Group = this.conta
/**
* 取得机器人的群成员列表
*/
inline val Bot.groups: ContactList<Group>
get() = this.contacts.groups
inline val Bot.groups: ContactList<Group> get() = this.contacts.groups
/**
* 取得机器人的好友列表
*/
inline val Bot.qqs: ContactList<QQ>
get() = this.contacts.qqs
inline val Bot.qqs: ContactList<QQ> get() = this.contacts.qqs
/**
* [BotSession] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.

View File

@ -1,48 +1,40 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate")
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.contact
import com.soywiz.klock.Date
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import net.mamoe.mirai.Bot
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.singleChain
import net.mamoe.mirai.network.BotSession
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsPacket
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsResponse
import net.mamoe.mirai.network.protocol.tim.packet.action.SendFriendMessagePacket
import net.mamoe.mirai.network.protocol.tim.packet.action.SendGroupMessagePacket
import net.mamoe.mirai.network.sessionKey
import net.mamoe.mirai.qqAccount
import net.mamoe.mirai.sendPacket
import net.mamoe.mirai.utils.SuspendLazy
import net.mamoe.mirai.utils.internal.PositiveNumbers
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
import net.mamoe.mirai.withSession
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class ContactList<C : Contact> : MutableMap<UInt, C> by mutableMapOf()
/**
* 联系人. 虽然叫做联系人, 但他的子类有 [QQ] [][Group].
*
* @param bot 这个联系人所属 [Bot]
* @param id 可以是 QQ 号码或者群号码 [GroupId].
*
* @author Him188moe
*/
sealed class Contact(val bot: Bot, val id: UInt) {
interface Contact {
/**
* 这个联系人所属 [Bot]
*/
val bot: Bot
/**
* 可以是 QQ 号码或者群号码 [GroupId].
*/
val id: UInt
/**
* 向这个对象发送消息.
*
* 速度太快会被服务器屏蔽(无响应). 在测试中不延迟地发送 6 条消息就会被屏蔽之后的数据包 1 秒左右.
*/
abstract suspend fun sendMessage(message: MessageChain)
suspend fun sendMessage(message: MessageChain)
//这两个方法应写为扩展函数, 但为方便 import 还是写在这里
@ -51,62 +43,6 @@ sealed class Contact(val bot: Bot, val id: UInt) {
suspend fun sendMessage(message: Message) = sendMessage(message.singleChain())
}
/**
* 一般的用户可见的 ID.
* TIM/QQ 客户端中所看到的的号码均是这个 ID.
*
* : 在引用群 ID , 应使用 [GroupId] [GroupInternalId] 类型, 而不是 [UInt]
*
* @see GroupInternalId.toId [GroupInternalId] 转换为 [GroupId]
* @see GroupId.toInternalId [GroupId] 转换为 [GroupInternalId]
*/
inline class GroupId(inline val value: UInt)
/**
* [this] 转为 [GroupId].
*/
fun UInt.groupId(): GroupId = GroupId(this)
/**
* 将无符号整数格式的 [Long] 转为 [GroupId].
*
* : Java 中常用 [Long] 来表示 [UInt]
*/
fun @receiver:PositiveNumbers Long.groupId(): GroupId = GroupId(this.coerceAtLeastOrFail(0).toUInt())
/**
* 一些群 API 使用的 ID. 在使用时会特别注明
*
* : 在引用群 ID , 应使用 [GroupId] [GroupInternalId] 类型, 而不是 [UInt]
*
* @see GroupInternalId.toId [GroupInternalId] 转换为 [GroupId]
* @see GroupId.toInternalId [GroupId] 转换为 [GroupInternalId]
*/
inline class GroupInternalId(inline val value: UInt)
/**
* .
*
* Group ID Group Number 并不是同一个值.
* - Group Number([Group.id]) 是通常使用的群号码.( QQ 客户端中可见)
* - Group ID([Group.internalId]) 是与调用 API 时使用的 id.( QQ 客户端中不可见)
* @author Him188moe
*/
@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")
class Group internal constructor(bot: Bot, val groupId: GroupId) : Contact(bot, groupId.value) {
val internalId = GroupId(id).toInternalId()
val members: ContactList<Member>
get() = TODO("Implementing group members is less important")
override suspend fun sendMessage(message: MessageChain) {
bot.sendPacket(SendGroupMessagePacket(bot.qqAccount, internalId, bot.sessionKey, message))
}
override fun toString(): String = "Group(${this.id})"
companion object
}
/**
* [BotSession] 作为接收器 (receiver) 并调用 [block], 返回 [block] 的返回值.
* 这个方法将能帮助使用在 [BotSession] 中定义的一些扩展方法, [BotSession.sendAndExpectAsync]
@ -120,117 +56,11 @@ inline fun <R> Contact.withSession(block: BotSession.() -> R): R {
}
/**
* QQ 对象.
* 注意: 一个 [QQ] 实例并不是独立的, 它属于一个 [Bot].
* 它不能被直接构造. 任何时候都应从 [Bot.qq], [Bot.ContactSystem.getQQ], [BotSession.qq] 或事件中获取.
*
* 对于同一个 [Bot] 任何一个人的 [QQ] 实例都是单一的.
*
* A QQ instance helps you to receive event from or sendPacket event to.
* Notice that, one QQ instance belong to one [Bot], that is, QQ instances from different [Bot] are NOT the same.
*
* @author Him188moe
* 只读联系人列表
*/
open class QQ internal constructor(bot: Bot, id: UInt) : Contact(bot, id) {
private var _profile: Profile? = null
private val _initialProfile by bot.network.SuspendLazy { updateProfile() }
/**
* 用户资料.
*/
val profile: Deferred<Profile>
get() = if (_profile == null) _initialProfile else CompletableDeferred(_profile!!)
override suspend fun sendMessage(message: MessageChain) =
bot.sendPacket(SendFriendMessagePacket(bot.qqAccount, id, bot.sessionKey, message))
/**
* 更新个人资料.
* 将会同步更新 property [profile]
*/
suspend fun updateProfile(): Profile = bot.withSession {
_profile = RequestProfileDetailsPacket(bot.qqAccount, id, sessionKey)
.sendAndExpect<RequestProfileDetailsResponse, Profile> { it.profile }
return _profile!!
}
override fun toString(): String = "QQ(${this.id})"
}
class ContactList<C : Contact> internal constructor(private val delegate: MutableContactList<C>) : Map<UInt, C> by delegate
/**
* 群成员
* 可修改联系人列表. 只会在内部使用.
*/
class Member internal constructor(bot: Bot, id: UInt, val group: Group) : QQ(bot, id) {
init {
TODO("Group member implementation")
}
override fun toString(): String = "Member(${this.id})"
}
/**
* 群成员的权限
*/
enum class MemberPermission {
/**
* 群主
*/
OWNER,
/**
* 管理员
*/
OPERATOR,
/**
* 一般群成员
*/
MEMBER;
}
/**
* 个人资料
*/
@Suppress("PropertyName")
data class Profile(
val qq: UInt,
val nickname: String,
val englishName: String?,
val chineseName: String?,
val qAge: Int?, // q 龄
val zipCode: String?,
val phone: String?,
val gender: Gender,
val birthday: Date?,
val personalStatement: String?,// 个人说明
val school: String?,
val homepage: String?,
val email: String?,
val company: String?
) {
override fun toString(): String = "Profile(qq=$qq, " +
"nickname=$nickname, " +
"gender=$gender, " +
(englishName?.let { "englishName=$englishName, " } ?: "") +
(chineseName?.let { "chineseName=$chineseName, " } ?: "") +
(qAge?.toString()?.let { "qAge=$qAge, " } ?: "") +
(zipCode?.let { "zipCode=$zipCode, " } ?: "") +
(phone?.let { "phone=$phone, " } ?: "") +
(birthday?.toString()?.let { "birthday=$birthday, " } ?: "") +
(personalStatement?.let { "personalStatement=$personalStatement, " } ?: "") +
(school?.let { "school=$school, " } ?: "") +
(homepage?.let { "homepage=$homepage, " } ?: "") +
(email?.let { "email=$email, " } ?: "") +
(company?.let { "company=$company," } ?: "") +
")"// 最终会是 ", )", 但这并不影响什么.
}
/**
* 性别
*/
enum class Gender {
SECRET,
MALE,
FEMALE;
}
internal class MutableContactList<C : Contact> : MutableMap<UInt, C> by mutableMapOf()

View File

@ -0,0 +1,57 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.contact
import net.mamoe.mirai.utils.internal.PositiveNumbers
import net.mamoe.mirai.utils.internal.coerceAtLeastOrFail
/**
* .
*
* Group ID Group Number 并不是同一个值.
* - Group Number([Group.id]) 是通常使用的群号码.( QQ 客户端中可见)
* - Group ID([Group.internalId]) 是与调用 API 时使用的 id.( QQ 客户端中不可见)
* @author Him188moe
*/
interface Group : Contact {
val internalId: GroupInternalId
val member: ContactList<Member>
suspend fun getMember(id: UInt): Member
}
/**
* 一般的用户可见的 ID.
* TIM/QQ 客户端中所看到的的号码均是这个 ID.
*
* : 在引用群 ID , 应使用 [GroupId] [GroupInternalId] 类型, 而不是 [UInt]
*
* @see GroupInternalId.toId [GroupInternalId] 转换为 [GroupId]
* @see GroupId.toInternalId [GroupId] 转换为 [GroupInternalId]
*/
inline class GroupId(inline val value: UInt)
/**
* [this] 转为 [GroupId].
*/
fun UInt.groupId(): GroupId = GroupId(this)
/**
* 将无符号整数格式的 [Long] 转为 [GroupId].
*
* : Java 中常用 [Long] 来表示 [UInt]
*/
fun @receiver:PositiveNumbers Long.groupId(): GroupId =
GroupId(this.coerceAtLeastOrFail(0).toUInt())
/**
* 一些群 API 使用的 ID. 在使用时会特别注明
*
* : 在引用群 ID , 应使用 [GroupId] [GroupInternalId] 类型, 而不是 [UInt]
*
* @see GroupInternalId.toId [GroupInternalId] 转换为 [GroupId]
* @see GroupId.toInternalId [GroupId] 转换为 [GroupInternalId]
*/
inline class GroupInternalId(inline val value: UInt)

View File

@ -0,0 +1,31 @@
package net.mamoe.mirai.contact
/**
* 群成员.
*
* 使用 [QQ.equals]. 因此同 ID 的群成员和 QQ `==`
*/
interface Member : QQ, Contact {
/**
* 所在的群
*/
val group: Group
}
/**
* 群成员的权限
*/
enum class MemberPermission {
/**
* 群主
*/
OWNER,
/**
* 管理员
*/
OPERATOR,
/**
* 一般群成员
*/
MEMBER;
}

View File

@ -0,0 +1,33 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.contact
import kotlinx.coroutines.Deferred
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.data.Profile
import net.mamoe.mirai.network.BotSession
/**
* QQ 对象.
* 注意: 一个 [QQ] 实例并不是独立的, 它属于一个 [Bot].
* 它不能被直接构造. 任何时候都应从 [Bot.qq], [Bot.ContactSystem.getQQ], [BotSession.qq] 或事件中获取.
*
* 对于同一个 [Bot] 任何一个人的 [QQ] 实例都是单一的.
*
* A QQ instance helps you to receive event from or sendPacket event to.
* Notice that, one QQ instance belong to one [Bot], that is, QQ instances from different [Bot] are NOT the same.
*
* @author Him188moe
*/
interface QQ : Contact {
/**
* 用户资料.
*/
val profile: Deferred<Profile>
/**
* 更新个人资料.
* 将会同步更新 property [profile]
*/
suspend fun updateProfile(): Profile
}

View File

@ -0,0 +1,52 @@
@file:Suppress("EXPERIMENTAL_API_USAGE")
package net.mamoe.mirai.contact.data
import com.soywiz.klock.Date
/**
* 个人资料
*/
@Suppress("PropertyName")
data class Profile(
val qq: UInt,
val nickname: String,
val englishName: String?,
val chineseName: String?,
val qAge: Int?, // q 龄
val zipCode: String?,
val phone: String?,
val gender: Gender,
val birthday: Date?,
val personalStatement: String?,// 个人说明
val school: String?,
val homepage: String?,
val email: String?,
val company: String?
) {
override fun toString(): String = "Profile(qq=$qq, " +
"nickname=$nickname, " +
"gender=$gender, " +
(englishName?.let { "englishName=$englishName, " } ?: "") +
(chineseName?.let { "chineseName=$chineseName, " } ?: "") +
(qAge?.toString()?.let { "qAge=$qAge, " } ?: "") +
(zipCode?.let { "zipCode=$zipCode, " } ?: "") +
(phone?.let { "phone=$phone, " } ?: "") +
(birthday?.toString()?.let { "birthday=$birthday, " } ?: "") +
(personalStatement?.let { "personalStatement=$personalStatement, " } ?: "") +
(school?.let { "school=$school, " } ?: "") +
(homepage?.let { "homepage=$homepage, " } ?: "") +
(email?.let { "email=$email, " } ?: "") +
(company?.let { "company=$company," } ?: "") +
")"// 最终会是 ", )", 但这并不影响什么.
}
/**
* 性别
*/
enum class Gender {
SECRET,
MALE,
FEMALE;
}

View File

@ -0,0 +1,87 @@
@file:Suppress("EXPERIMENTAL_API_USAGE", "unused", "MemberVisibilityCanBePrivate")
package net.mamoe.mirai.contact.internal
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.mamoe.mirai.*
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.data.Profile
import net.mamoe.mirai.message.Message
import net.mamoe.mirai.message.MessageChain
import net.mamoe.mirai.message.singleChain
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsPacket
import net.mamoe.mirai.network.protocol.tim.packet.action.RequestProfileDetailsResponse
import net.mamoe.mirai.network.protocol.tim.packet.action.SendFriendMessagePacket
import net.mamoe.mirai.network.protocol.tim.packet.action.SendGroupMessagePacket
import net.mamoe.mirai.network.sessionKey
import net.mamoe.mirai.utils.SuspendLazy
internal sealed class ContactImpl : Contact {
abstract override suspend fun sendMessage(message: MessageChain)
//这两个方法应写为扩展函数, 但为方便 import 还是写在这里
override suspend fun sendMessage(plain: String) = sendMessage(plain.singleChain())
override suspend fun sendMessage(message: Message) = sendMessage(message.singleChain())
}
@Suppress("MemberVisibilityCanBePrivate", "CanBeParameter")
internal 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()
override suspend fun getMember(id: UInt): Member =
if (_members.containsKey(id)) _members[id]!!
else membersLock.withLock {
_members.getOrPut(id) { MemberImpl(bot, bot.getQQ(id), this) }
}
override suspend fun sendMessage(message: MessageChain) {
bot.sendPacket(SendGroupMessagePacket(bot.qqAccount, internalId, bot.sessionKey, message))
}
override fun toString(): String = "Group(${this.id})"
}
internal class QQImpl internal constructor(override val bot: Bot, override val id: UInt) : ContactImpl(), QQ {
private var _profile: Profile? = null
private val _initialProfile by bot.network.SuspendLazy { updateProfile() }
override val profile: Deferred<Profile> get() = if (_profile == null) _initialProfile else CompletableDeferred(_profile!!)
override suspend fun sendMessage(message: MessageChain) =
bot.sendPacket(SendFriendMessagePacket(bot.qqAccount, id, bot.sessionKey, message))
/**
* 更新个人资料.
* 将会同步更新 property [profile]
*/
override suspend fun updateProfile(): Profile = bot.withSession {
_profile = RequestProfileDetailsPacket(bot.qqAccount, id, sessionKey)
.sendAndExpect<RequestProfileDetailsResponse, Profile> { it.profile }
return _profile!!
}
override fun toString(): String = "QQ(${this.id})"
}
/**
* 群成员
*/
internal class MemberImpl(override val bot: Bot, private val delegate: QQ, override val group: Group) : Member, ContactImpl() {
override val profile: Deferred<Profile> get() = delegate.profile
override val id: UInt get() = delegate.id
override suspend fun updateProfile(): Profile = delegate.updateProfile()
override suspend fun sendMessage(message: MessageChain) = delegate.sendMessage(message)
override fun toString(): String = "Member(${this.id})"
}

View File

@ -170,8 +170,6 @@ inline class ImageId0x06(override inline val value: String) : ImageId {
/**
* 一般是群的图片的 id.
*
* @param md5 用于下载图片时提交
*/
class ImageId0x03 constructor(override inline val value: String, inline val uniqueId: UInt, inline val height: Int, inline val width: Int) : ImageId {
override fun toString(): String = "ImageId(value=$value, uniqueId=${uniqueId}, height=$height, width=$width)"

View File

@ -4,8 +4,8 @@ package net.mamoe.mirai.network.protocol.tim.packet.action
import com.soywiz.klock.Date
import kotlinx.io.core.*
import net.mamoe.mirai.contact.Gender
import net.mamoe.mirai.contact.Profile
import net.mamoe.mirai.contact.data.Gender
import net.mamoe.mirai.contact.data.Profile
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.packet.*
import net.mamoe.mirai.utils.io.*

View File

@ -64,7 +64,6 @@ object EventPacketFactory : PacketFactory<Packet, SessionKey>(SessionKey) {
}
}
operator fun invoke(
id: PacketId,
sequenceId: UShort,

View File

@ -0,0 +1,25 @@
@file:Suppress("EXPERIMENTAL_UNSIGNED_LITERALS")
package net.mamoe.mirai.network.protocol.tim.packet.event
import kotlinx.io.core.ByteReadPacket
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.Member
import net.mamoe.mirai.network.protocol.tim.packet.PacketVersion
/**
* 成员被踢出
*/
data class MemberKickEvent(
val member: Member
) : EventPacket
@PacketVersion(date = "2019.11.20", timVersion = "2.3.2 (21173)")
object MemberKickEventPacketFactory : KnownEventParserAndHandler<MemberKickEvent>(0x0022u) {
override suspend fun ByteReadPacket.parse(bot: Bot, identity: EventPacketIdentity): MemberKickEvent {
TODO()
// return MemberKickEvent()
}
}

View File

@ -3,7 +3,7 @@
package net.mamoe.mirai.network.protocol.tim.packet.login
import kotlinx.io.core.*
import net.mamoe.mirai.contact.Gender
import net.mamoe.mirai.contact.data.Gender
import net.mamoe.mirai.network.BotNetworkHandler
import net.mamoe.mirai.network.protocol.tim.TIMProtocol
import net.mamoe.mirai.network.protocol.tim.packet.*

View File

@ -61,6 +61,7 @@ class ExternalImage(
suspend fun ExternalImage.sendTo(contact: Contact) = when (contact) {
is Group -> contact.uploadImage(this).sendTo(contact)
is QQ -> contact.uploadImage(this).sendTo(contact)
else -> assertUnreachable()
}
/**
@ -72,6 +73,7 @@ suspend fun ExternalImage.sendTo(contact: Contact) = when (contact) {
suspend fun ExternalImage.upload(contact: Contact): Image = when (contact) {
is Group -> contact.uploadImage(this).image()
is QQ -> contact.uploadImage(this).image()
else -> assertUnreachable()
}
/**