[core] Support friend group (#2113)

* feat: support friend group

* remove unnecessary modifications

* toByteArray2

* support friendGroup, with api dump

* support rename, with api dump

* modify as required

* modify as required

* reverse

* doc

* FriendGroups

* api dump

* modify as required

* fix CI

* FriendGroup sync notice

* api dump

* modify as required

* immutable

* add friends: ContactList in FriendGroup

* more sync notice

* modify log content

* Change `FriendGroup.friends` to `Collection<Friend>`

* Fix `FriendGroup.friends.isEmpty()`

* modified as require, untested

* del count and online count in info

* change import

* fix missing import

* set @since 2.13 and modified as required

* modified as required

* modified as required

* doc

* change friendGroupId type to Int?

* api dumped

* change friendGroupId type to Int?

* introduce null to friendGroupId

* modified as required

* chore

* api dump

* chore: remark

* change int? to int

* api dump

* Update mirai-core-api/src/commonMain/kotlin/data/FriendGroups.kt

Co-authored-by: Him188 <Him188@mamoe.net>

* Move FriendGroup and FriendGroups to contact.friendgroup

* Make `Friend.friendGroup` not null

* add FriendGroups.default for default group

* Redesign FriendGroup interface

Co-authored-by: Karlatemp <kar@kasukusakura.com>
Co-authored-by: Him188 <Him188@mamoe.net>
This commit is contained in:
Eritque arcus 2022-08-26 04:56:09 -04:00 committed by GitHub
parent eb892da093
commit fea1d28488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 734 additions and 27 deletions

View File

@ -15,6 +15,7 @@ public abstract interface class net/mamoe/mirai/Bot : kotlinx/coroutines/Corouti
public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
public abstract fun getEventChannel ()Lnet/mamoe/mirai/event/EventChannel;
public fun getFriend (J)Lnet/mamoe/mirai/contact/Friend;
public abstract fun getFriendGroups ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroups;
public fun getFriendOrFail (J)Lnet/mamoe/mirai/contact/Friend;
public abstract fun getFriends ()Lnet/mamoe/mirai/contact/ContactList;
public fun getGroup (J)Lnet/mamoe/mirai/contact/Group;
@ -354,6 +355,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/roaming/RoamingSupported {
public fun delete ()V
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getFriendGroup ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public abstract fun getRemark ()Ljava/lang/String;
public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
public synthetic fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
@ -908,6 +910,27 @@ public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles {
public static synthetic fun uploadNewFile$suspendImpl (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/friendgroup/FriendGroup {
public fun delete ()Z
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getCount ()I
public abstract fun getFriends ()Ljava/util/Collection;
public abstract fun getId ()I
public abstract fun getName ()Ljava/lang/String;
public fun moveIn (Lnet/mamoe/mirai/contact/Friend;)Z
public abstract fun moveIn (Lnet/mamoe/mirai/contact/Friend;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun renameTo (Ljava/lang/String;)Z
public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/friendgroup/FriendGroups {
public abstract fun asCollection ()Ljava/util/Collection;
public fun create (Ljava/lang/String;)Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public abstract fun create (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun get (I)Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public fun getDefault ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
}
public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
@ -962,6 +985,7 @@ public abstract interface class net/mamoe/mirai/contact/roaming/RoamingSupported
}
public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
public abstract fun getFriendGroupId ()I
public abstract fun getNick ()Ljava/lang/String;
public abstract fun getRemark ()Ljava/lang/String;
public abstract fun getUin ()J
@ -970,9 +994,11 @@ public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mira
public class net/mamoe/mirai/data/FriendInfoImpl : net/mamoe/mirai/data/FriendInfo {
public fun <init> (JLjava/lang/String;Ljava/lang/String;)V
public fun getFriendGroupId ()I
public fun getNick ()Ljava/lang/String;
public fun getRemark ()Ljava/lang/String;
public fun getUin ()J
public fun setFriendGroupId (I)V
public fun setNick (Ljava/lang/String;)V
public fun setRemark (Ljava/lang/String;)V
}
@ -1688,6 +1714,7 @@ public abstract interface class net/mamoe/mirai/data/UserInfo {
public abstract interface class net/mamoe/mirai/data/UserProfile {
public abstract fun getAge ()I
public abstract fun getEmail ()Ljava/lang/String;
public abstract fun getFriendGroupId ()I
public abstract fun getNickname ()Ljava/lang/String;
public abstract fun getQLevel ()I
public abstract fun getSex ()Lnet/mamoe/mirai/data/UserProfile$Sex;

View File

@ -15,6 +15,7 @@ public abstract interface class net/mamoe/mirai/Bot : kotlinx/coroutines/Corouti
public abstract fun getConfiguration ()Lnet/mamoe/mirai/utils/BotConfiguration;
public abstract fun getEventChannel ()Lnet/mamoe/mirai/event/EventChannel;
public fun getFriend (J)Lnet/mamoe/mirai/contact/Friend;
public abstract fun getFriendGroups ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroups;
public fun getFriendOrFail (J)Lnet/mamoe/mirai/contact/Friend;
public abstract fun getFriends ()Lnet/mamoe/mirai/contact/ContactList;
public fun getGroup (J)Lnet/mamoe/mirai/contact/Group;
@ -354,6 +355,7 @@ public abstract interface class net/mamoe/mirai/contact/FileSupported : net/mamo
public abstract interface class net/mamoe/mirai/contact/Friend : kotlinx/coroutines/CoroutineScope, net/mamoe/mirai/contact/AudioSupported, net/mamoe/mirai/contact/User, net/mamoe/mirai/contact/roaming/RoamingSupported {
public fun delete ()V
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getFriendGroup ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public abstract fun getRemark ()Ljava/lang/String;
public fun nudge ()Lnet/mamoe/mirai/message/action/FriendNudge;
public synthetic fun nudge ()Lnet/mamoe/mirai/message/action/Nudge;
@ -908,6 +910,27 @@ public abstract interface class net/mamoe/mirai/contact/file/RemoteFiles {
public static synthetic fun uploadNewFile$suspendImpl (Lnet/mamoe/mirai/contact/file/RemoteFiles;Ljava/lang/String;Lnet/mamoe/mirai/utils/ExternalResource;Lnet/mamoe/mirai/utils/ProgressionCallback;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/friendgroup/FriendGroup {
public fun delete ()Z
public abstract fun delete (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun getCount ()I
public abstract fun getFriends ()Ljava/util/Collection;
public abstract fun getId ()I
public abstract fun getName ()Ljava/lang/String;
public fun moveIn (Lnet/mamoe/mirai/contact/Friend;)Z
public abstract fun moveIn (Lnet/mamoe/mirai/contact/Friend;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun renameTo (Ljava/lang/String;)Z
public abstract fun renameTo (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class net/mamoe/mirai/contact/friendgroup/FriendGroups {
public abstract fun asCollection ()Ljava/util/Collection;
public fun create (Ljava/lang/String;)Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public abstract fun create (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun get (I)Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
public fun getDefault ()Lnet/mamoe/mirai/contact/friendgroup/FriendGroup;
}
public abstract interface class net/mamoe/mirai/contact/roaming/RoamingMessage {
public fun getBot ()Lnet/mamoe/mirai/Bot;
public abstract fun getContact ()Lnet/mamoe/mirai/contact/Contact;
@ -962,6 +985,7 @@ public abstract interface class net/mamoe/mirai/contact/roaming/RoamingSupported
}
public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mirai/data/UserInfo {
public abstract fun getFriendGroupId ()I
public abstract fun getNick ()Ljava/lang/String;
public abstract fun getRemark ()Ljava/lang/String;
public abstract fun getUin ()J
@ -970,9 +994,11 @@ public abstract interface class net/mamoe/mirai/data/FriendInfo : net/mamoe/mira
public class net/mamoe/mirai/data/FriendInfoImpl : net/mamoe/mirai/data/FriendInfo {
public fun <init> (JLjava/lang/String;Ljava/lang/String;)V
public fun getFriendGroupId ()I
public fun getNick ()Ljava/lang/String;
public fun getRemark ()Ljava/lang/String;
public fun getUin ()J
public fun setFriendGroupId (I)V
public fun setNick (Ljava/lang/String;)V
public fun setRemark (Ljava/lang/String;)V
}
@ -1688,6 +1714,7 @@ public abstract interface class net/mamoe/mirai/data/UserInfo {
public abstract interface class net/mamoe/mirai/data/UserProfile {
public abstract fun getAge ()I
public abstract fun getEmail ()Ljava/lang/String;
public abstract fun getFriendGroupId ()I
public abstract fun getNickname ()Ljava/lang/String;
public abstract fun getQLevel ()I
public abstract fun getSex ()Lnet/mamoe/mirai/data/UserProfile$Sex;

View File

@ -15,6 +15,7 @@ package net.mamoe.mirai
import kotlinx.coroutines.*
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.*
import net.mamoe.mirai.contact.friendgroup.FriendGroups
import net.mamoe.mirai.event.EventChannel
import net.mamoe.mirai.event.events.BotEvent
import net.mamoe.mirai.message.action.BotNudge
@ -112,6 +113,13 @@ public interface Bot : CoroutineScope, ContactOrBot, UserOrBot {
*/
public val friends: ContactList<Friend>
/**
* 全部的好友分组
*
* @since 2.13
*/
public val friendGroups: FriendGroups
/**
* [对方 QQ 号码][id] 获取一个好友对象, 在获取失败时返回 `null`.

View File

@ -15,6 +15,7 @@ package net.mamoe.mirai.contact
import kotlinx.coroutines.CoroutineScope
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.Bot
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.contact.roaming.RoamingSupported
import net.mamoe.mirai.event.events.*
import net.mamoe.mirai.message.MessageReceipt
@ -38,6 +39,14 @@ import net.mamoe.mirai.utils.NotStableForInheritance
@NotStableForInheritance
public interface Friend : User, CoroutineScope, AudioSupported, RoamingSupported {
/**
* 该好友所在的好友分组
*
* @since 2.13
*/
public val friendGroup: FriendGroup
/**
* 备注信息
*

View File

@ -0,0 +1,77 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.contact.friendgroup
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 一个好友分组.
* 可能同时存在多个相同[名称][name]的分组, 但是每个分组的 [id] 都是唯一的.
*
*
* 要获取一个分组, 可使用 [get] 根据 [ID][FriendGroup.id] 获取, 或者使用 [asCollection] 获取全部分组列表.
* 也可以通过 [Friend.friendGroup] 获取一个好友所在的分组.
*
* 在每次登录会话中, [FriendGroup] 的实例是依据 [id] 唯一的. 存在于同一个分组中的多个好友的 [Friend.friendGroup] 会返回相同的 [FriendGroup] 实例.
* 但当 bot 重新登录后, 实例可能变化.
*
* @see FriendGroups
* @since 2.13
*/
@JvmBlockingBridge
@NotStableForInheritance
public interface FriendGroup {
/**
* 好友分组 ID
*/
public val id: Int
/**
* 好友分组名
*/
public val name: String
/**
* 好友分组内好友数量
*/
public val count: Int
/**
* 属于本分组的好友集合
*/
public val friends: Collection<Friend>
/**
* 更改好友分组名称.
*
* 允许存在同名分组.
* 当操作成时返回 `true`; 当分组不存在时返回 `false`; 如果因为其他原因造成的改名失败时会抛出 [IllegalStateException]
*/
public suspend fun renameTo(newName: String): Boolean
/**
* 把一名好友移动至本分组内.
*
* 当远程分组不存在时会自动移动该好友到 ID 0 的默认好友分组.
* 当操作成功时返回 `true`; 当分组不存在 (如已经在远程被删除) 时返回 `false`; 因为其他原因移动不成功时抛出 [IllegalStateException].
*/
public suspend fun moveIn(friend: Friend): Boolean
/**
* 删除本分组.
*
* 删除后组内全部好友移动至 ID 0 的默认好友分组, 本分组的好友列表会被清空.
* 当操作成功时返回 `true`; 当分组不存在或试图删除 ID 0 的默认好友分组时返回 `false`;
* 因为其他原因删除不成功时抛出 [IllegalStateException].
*/
public suspend fun delete(): Boolean
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.contact.friendgroup
import me.him188.kotlin.jvm.blocking.bridge.JvmBlockingBridge
import net.mamoe.mirai.utils.NotStableForInheritance
/**
* 好友分组列表 (管理器).
* 允许存在重复名称的分组, 因此依赖于 name 判断不可靠, 需要依赖 ID 判断.
*
* @see FriendGroup
* @since 2.13
*/
@JvmBlockingBridge
@NotStableForInheritance
public interface FriendGroups {
/**
* 获取 [ID][FriendGroup.id] `0` 的默认分组 ("我的好友").
*/
public val default: FriendGroup get() = get(0) ?: error("Internal error: could not find FriendGroup with id = 0.")
/**
* 新建一个好友分组.
*
* 允许名称重复, 当新建一个已存在名称的分组时, 服务器会返回一个拥有重复名字的新分组;
* 当因为其他原因创建不成功时抛出 [IllegalStateException].
*
* 提示: 要删除一个好友分组, 使用 [FriendGroup.delete].
*/
public suspend fun create(name: String): FriendGroup
/**
* 获取指定 ID 的好友分组, 不存在时返回 `null`
*/
public operator fun get(id: Int): FriendGroup?
/**
* 获取包含全部 [FriendGroup] [Collection]. 返回的 [Collection] 只可读取.
*
* 此方法快速返回, 不会在调用时实例化新的 [Collection] 对象.
* 返回的 [Collection] 是对缓存的引用, 会随着服务器通知和机器人操作 ( [create]) 变化.
*/
public fun asCollection(): Collection<FriendGroup>
}

View File

@ -18,6 +18,8 @@ public interface FriendInfo : UserInfo {
public override val nick: String
public override var remark: String
public val friendGroupId: Int
}
@ -32,4 +34,6 @@ public open class FriendInfoImpl(
override val uin: Long,
override var nick: String,
override var remark: String,
) : FriendInfo
) : FriendInfo {
override var friendGroupId: Int = 0
}

View File

@ -1,10 +1,10 @@
/*
* Copyright 2019-2021 Mamoe Technologies and contributors.
* Copyright 2019-2022 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.
* 此源代码的使用受 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
* https://github.com/mamoe/mirai/blob/dev/LICENSE
*/
package net.mamoe.mirai.data
@ -27,6 +27,13 @@ public interface UserProfile {
public val qLevel: Int
public val sex: Sex
/**
* 好友分组 ID, 在非好友情况下或者位于默认分组情况下为 `0`
*
* @since 2.13
*/
public val friendGroupId: Int
/**
* 个性签名
*/

View File

@ -100,7 +100,7 @@ internal abstract class AbstractBot constructor(
final override val strangers: ContactList<StrangerImpl> = ContactList()
final override val asFriend: FriendImpl by lazy {
Mirai.newFriend(this, FriendInfoImpl(uin, "", "")).cast()
Mirai.newFriend(this, FriendInfoImpl(uin, "", "", 0)).cast()
} // nick is initialized later on login
final override val asStranger: StrangerImpl by lazy {
Mirai.newStranger(this, StrangerInfoImpl(bot.id, bot.nick)).cast()

View File

@ -599,7 +599,7 @@ internal open class MiraiImpl : IMirai, LowLevelApiAccessor {
if (!accept) return
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
bot.friends.delegate.add(newFriend(bot, FriendInfoImpl(fromId, fromNick, "")))
bot.friends.delegate.add(newFriend(bot, FriendInfoImpl(fromId, fromNick, "", 0)))
}
override suspend fun solveBotInvitedJoinGroupRequestEvent(

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.BotOfflineEvent
import net.mamoe.mirai.event.events.BotOnlineEvent
import net.mamoe.mirai.event.events.BotReloginEvent
import net.mamoe.mirai.internal.contact.friendgroup.FriendGroupsImpl
import net.mamoe.mirai.internal.network.component.ComponentStorage
import net.mamoe.mirai.internal.network.component.ComponentStorageDelegate
import net.mamoe.mirai.internal.network.component.ConcurrentComponentStorage
@ -44,6 +45,7 @@ import net.mamoe.mirai.internal.network.notice.group.GroupMessageProcessor
import net.mamoe.mirai.internal.network.notice.group.GroupNotificationProcessor
import net.mamoe.mirai.internal.network.notice.group.GroupOrMemberListNoticeProcessor
import net.mamoe.mirai.internal.network.notice.group.GroupRecallProcessor
import net.mamoe.mirai.internal.network.notice.priv.FriendGroupNoticeProcessor
import net.mamoe.mirai.internal.network.notice.priv.FriendNoticeProcessor
import net.mamoe.mirai.internal.network.notice.priv.OtherClientNoticeProcessor
import net.mamoe.mirai.internal.network.notice.priv.PrivateMessageProcessor
@ -71,6 +73,8 @@ internal open class QQAndroidBot constructor(
configuration: BotConfiguration,
) : AbstractBot(configuration, account.id) {
override val bot: QQAndroidBot get() = this
override val friendGroups: FriendGroupsImpl by lazy { FriendGroupsImpl(this) }
val client get() = components[SsoProcessor].client
override fun close(cause: Throwable?) {
@ -191,6 +195,7 @@ internal open class QQAndroidBot constructor(
GroupOrMemberListNoticeProcessor(pipelineLogger.subLogger("GroupOrMemberListNoticeProcessor")),
GroupMessageProcessor(pipelineLogger.subLogger("GroupMessageProcessor")),
GroupNotificationProcessor(pipelineLogger.subLogger("GroupNotificationProcessor")),
FriendGroupNoticeProcessor(pipelineLogger.subLogger("FriendGroupNoticeProcessor")),
PrivateMessageProcessor(),
OtherClientNoticeProcessor(),
GroupRecallProcessor(),

View File

@ -14,11 +14,11 @@
package net.mamoe.mirai.internal.contact
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import io.ktor.utils.io.core.*
import kotlinx.coroutines.launch
import net.mamoe.mirai.LowLevelApi
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.contact.roaming.RoamingMessages
import net.mamoe.mirai.event.broadcast
import net.mamoe.mirai.event.events.FriendMessagePostSendEvent
@ -53,7 +53,8 @@ internal fun net.mamoe.mirai.internal.network.protocol.data.jce.FriendInfo.toMir
FriendInfoImpl(
friendUin,
nick,
remark
remark,
groupId.toInt(),
)
@OptIn(ExperimentalContracts::class)
@ -83,6 +84,9 @@ internal class FriendImpl(
}
}
override val friendGroup: FriendGroup
get() = bot.friendGroups[info.friendGroupId] ?: bot.friendGroups[0]!!
private val messageProtocolStrategy: MessageProtocolStrategy<FriendImpl> = FriendMessageProtocolStrategy(this)
override suspend fun delete() {

View File

@ -0,0 +1,111 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.friendgroup
import net.mamoe.mirai.contact.Friend
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.FriendImpl
import net.mamoe.mirai.internal.contact.impl
import net.mamoe.mirai.internal.contact.info.FriendGroupInfo
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
internal inline fun FriendGroup.impl(): FriendGroupImpl {
contract {
returns() implies (this@impl is FriendGroupImpl)
}
check(this is FriendGroupImpl) { "A FriendGroup instance is not instance of FriendGroupImpl. Your instance: ${this::class.qualifiedName}" }
return this
}
internal class FriendGroupImpl constructor(
val bot: QQAndroidBot,
val info: FriendGroupInfo
) : FriendGroup {
override val id: Int by info::groupId
override var name: String by info::groupName
override val count: Int
get() = friends.size
override val friends: Collection<Friend> = object : AbstractCollection<Friend>() {
override val size: Int
get() = bot.friends.count { it.impl().info.friendGroupId == id }
private val delegateSequence = sequence<Friend> {
bot.friends.forEach { friend ->
friend.impl()
if (friend.info.friendGroupId == id) {
yield(friend)
}
}
}
override fun iterator(): Iterator<Friend> = delegateSequence.iterator()
override fun isEmpty(): Boolean {
return bot.friends.none { it.impl().info.friendGroupId == id }
}
override fun contains(element: Friend): Boolean {
if (element !is FriendImpl) return false
return element.info.friendGroupId == id
}
}
override suspend fun renameTo(newName: String): Boolean {
bot.network.sendAndExpect(FriendList.SetGroupReqPack.Rename(bot.client, newName, id)).let {
if (it.result.toInt() == 1) {
return false
}
check(it.isSuccess) {
"Cannot rename friendGroup(id=$id) to $newName, code=${it.result.toInt()}, errStr=${it.errStr}"
}
}
info.groupName = newName
return true
}
override suspend fun moveIn(friend: Friend): Boolean {
bot.network.sendAndExpect(FriendList.MoveGroupMemReqPack(bot.client, friend.id, id)).let {
check(it.isSuccess) {
"Cannot move friend to $this, code=${it.result.toInt()}, errStr=${it.errStr}"
}
}
// 因为 MoveGroupMemReqPack 协议在测试里如果移动到不存在的分组,他会自动移动好友到 id = 0 的默认好友分组然后返回 result = 0
val id = friend.queryProfile().friendGroupId
friend.impl().info.friendGroupId = id
if (id != this.id && id == 0) return false
return true
}
override suspend fun delete(): Boolean {
bot.network.sendAndExpect(FriendList.SetGroupReqPack.Delete(bot.client, id)).let {
if (it.result.toInt() == 1) {
return false
}
check(it.isSuccess) {
"Cannot delete friendGroup, code=${it.result.toInt()}, errStr=${it.errStr}"
}
}
friends.forEach {
it.impl().info.friendGroupId = 0
}
bot.friendGroups.friendGroups.remove(this)
return true
}
override fun toString(): String {
return "FriendGroup(id=$id, name=$name)"
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.friendgroup
import net.mamoe.mirai.contact.friendgroup.FriendGroup
import net.mamoe.mirai.contact.friendgroup.FriendGroups
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.info.FriendGroupInfo
import net.mamoe.mirai.internal.network.protocol.packet.list.FriendList
import net.mamoe.mirai.utils.ConcurrentLinkedDeque
import net.mamoe.mirai.utils.asImmutable
internal class FriendGroupsImpl(
val bot: QQAndroidBot
) : FriendGroups {
val friendGroups = ConcurrentLinkedDeque<FriendGroupImpl>()
private val friendGroupsImmutable by lazy { friendGroups.asImmutable() }
override suspend fun create(name: String): FriendGroup {
val resp = bot.network.sendAndExpect(FriendList.SetGroupReqPack.New(bot.client, name))
check(resp.isSuccess) {
"Cannot create friendGroup, code=${resp.result.toInt()}, errStr=${resp.errStr}"
}
return FriendGroupImpl(bot, FriendGroupInfo(resp.groupId, name)).apply { friendGroups.add(this) }
}
override fun get(id: Int): FriendGroup? = friendGroups.firstOrNull { it.id == id }
override fun asCollection(): Collection<FriendGroup> = friendGroupsImmutable
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.contact.info
import kotlinx.serialization.Serializable
@Serializable
internal data class FriendGroupInfo(
val groupId: Int,
var groupName: String,
// val friendCount: Int,
// val onlineFriendCount: Int
)

View File

@ -18,8 +18,9 @@ internal data class FriendInfoImpl(
override val uin: Long,
override var nick: String,
override var remark: String,
override var friendGroupId: Int
) : FriendInfo {
companion object {
fun FriendInfo.impl() = if (this is FriendInfoImpl) this else FriendInfoImpl(uin, nick, remark)
fun FriendInfo.impl() = if (this is FriendInfoImpl) this else FriendInfoImpl(uin, nick, remark, friendGroupId)
}
}

View File

@ -23,10 +23,8 @@ import net.mamoe.mirai.data.MemberInfo
import net.mamoe.mirai.internal.QQAndroidBot
import net.mamoe.mirai.internal.contact.GroupImpl
import net.mamoe.mirai.internal.contact.StrangerImpl
import net.mamoe.mirai.internal.contact.info.FriendInfoImpl
import net.mamoe.mirai.internal.contact.info.GroupInfoImpl
import net.mamoe.mirai.internal.contact.info.MemberInfoImpl
import net.mamoe.mirai.internal.contact.info.StrangerInfoImpl
import net.mamoe.mirai.internal.contact.friendgroup.FriendGroupImpl
import net.mamoe.mirai.internal.contact.info.*
import net.mamoe.mirai.internal.contact.toMiraiFriendInfo
import net.mamoe.mirai.internal.network.component.ComponentKey
import net.mamoe.mirai.internal.network.component.ComponentStorage
@ -160,6 +158,38 @@ internal class ContactUpdaterImpl(
return friendInfos
}
suspend fun refreshFriendGroupList(): List<FriendGroupImpl> {
logger.info { "Start loading friendGroup list..." }
val friendGroupInfos = mutableListOf<FriendGroupImpl>()
var count = 0
var total: Short
while (true) {
val data = bot.network.sendAndExpect(
FriendList.GetFriendGroupList(bot.client, 0, 0, count, 150)
)
total = data.totoalGroupCount
for (jceInfo in data.groupList) {
friendGroupInfos.add(
FriendGroupImpl(
bot, FriendGroupInfo(
jceInfo.groupId.toInt(),
jceInfo.groupname
)
)
)
}
count += data.groupList.size
logger.verbose { "Loading friendGroup list: ${count}/${total}" }
if (count >= total) break
}
logger.info { "Successfully loaded friendGroup list: $count in total" }
return friendGroupInfos
}
val list = if (friendListCache?.isValid(registerResp) == true) {
val list = friendListCache.list
logger.info { "Loaded ${list.size} friends from local cache." }
@ -178,11 +208,13 @@ internal class ContactUpdaterImpl(
}
}
bot.friendGroups.friendGroups.clear()
bot.friendGroups.friendGroups.addAll(refreshFriendGroupList())
for (friendInfoImpl in list) {
bot.addNewFriendAndRemoveStranger(friendInfoImpl)
}
initFriendOk = true
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.notice.priv
import net.mamoe.mirai.internal.contact.friendgroup.FriendGroupImpl
import net.mamoe.mirai.internal.contact.friendgroup.impl
import net.mamoe.mirai.internal.contact.impl
import net.mamoe.mirai.internal.contact.info.FriendGroupInfo
import net.mamoe.mirai.internal.network.components.MixedNoticeProcessor
import net.mamoe.mirai.internal.network.components.NoticePipelineContext
import net.mamoe.mirai.internal.network.notice.NewContactSupport
import net.mamoe.mirai.internal.network.protocol.data.jce.MsgType0x210
import net.mamoe.mirai.internal.network.protocol.data.proto.Submsgtype0x27
import net.mamoe.mirai.internal.utils.io.serialization.loadAs
import net.mamoe.mirai.utils.MiraiLogger
import net.mamoe.mirai.utils.context
import net.mamoe.mirai.utils.error
import net.mamoe.mirai.utils.warning
internal class FriendGroupNoticeProcessor(
private val logger: MiraiLogger,
) : MixedNoticeProcessor(), NewContactSupport {
override suspend fun NoticePipelineContext.processImpl(data: MsgType0x210) = data.context {
when (data.uSubMsgType) {
0x27L -> {
val body = vProtobuf.loadAs(Submsgtype0x27.SubMsgType0x27.SubMsgType0x27MsgBody.serializer())
for (msgModInfo in body.msgModInfos) {
markAsConsumed(msgModInfo)
when {
msgModInfo.msgModFriendGroup != null -> handleFriendGroupChanged(
msgModInfo.msgModFriendGroup, logger
)
msgModInfo.msgModGroupName != null -> handleFriendGroupNameChanged(
msgModInfo.msgModGroupName, logger
)
msgModInfo.msgDelGroup != null -> handleDelGroup(msgModInfo.msgDelGroup, logger)
msgModInfo.msgAddGroup != null -> handleAddGroup(msgModInfo.msgAddGroup)
else -> markNotConsumed(msgModInfo)
}
}
}
}
}
private fun NoticePipelineContext.handleAddGroup(
addGroup: Submsgtype0x27.SubMsgType0x27.AddGroup
) {
bot.friendGroups.friendGroups.add(
FriendGroupImpl(
bot,
FriendGroupInfo(addGroup.groupid, addGroup.groupname.decodeToString())
)
)
}
private fun NoticePipelineContext.handleDelGroup(
delGroup: Submsgtype0x27.SubMsgType0x27.DelGroup, logger: MiraiLogger
) {
bot.friendGroups[delGroup.groupid]?.let { friendGroup ->
friendGroup.friends.forEach {
it.impl().info.friendGroupId = 0
}
bot.friendGroups.friendGroups.remove(friendGroup)
} ?: let {
logger.warning { "Detected friendGroup(id=${delGroup.groupid}) was removed but it isn't available in bot's friendGroups list" }
return
}
}
private fun NoticePipelineContext.handleFriendGroupNameChanged(
modFriendGroup: Submsgtype0x27.SubMsgType0x27.ModGroupName, logger: MiraiLogger
) {
bot.friendGroups[modFriendGroup.groupid]?.let {
it.impl().name = modFriendGroup.groupname.decodeToString()
} ?: let {
logger.warning { "Detected friendGroup(id=${modFriendGroup.groupid}) was renamed but it cannot be found in bot's friendGroups list" }
return
}
}
private fun NoticePipelineContext.handleFriendGroupChanged(
modFriendGroup: Submsgtype0x27.SubMsgType0x27.ModFriendGroup,
logger: MiraiLogger
) {
modFriendGroup.msgFrdGroup.forEach { body ->
val friend = bot.getFriend(body.fuin) ?: let {
logger.error { "Detected friend(id=${body.fuin}) was moved to friendGroup(id=${body.uint32NewGroupId}) but friend not found in bot's friends list" }
return
}
if (friend.impl().info.friendGroupId == body.uint32NewGroupId.first()) return@forEach
friend.info.friendGroupId = body.uint32NewGroupId.first()
}
}
}

View File

@ -264,6 +264,7 @@ internal class FriendNoticeProcessor(
uin = body.msgAddFrdNotify.fuin,
nick = body.msgAddFrdNotify.fuinNick,
remark = "",
friendGroupId = 0,
)
val removed = bot.removeStranger(info.uin)

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.data.jce
import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
import kotlin.jvm.JvmField
@Serializable
internal class MovGroupMemReq(
@JvmField @TarsId(0) val uin: Long = 0L,
@JvmField @TarsId(1) val reqtype: Byte = 0,
@JvmField @TarsId(2) val vecBody: ByteArray? = null
) : JceStruct
@Serializable
internal class MovGroupMemResp(
@JvmField @TarsId(0) val uin: Long = 0L,
@JvmField @TarsId(1) val reqtype: Byte = 0,
@JvmField @TarsId(2) val result: Byte = 0,
@JvmField @TarsId(3) val errorString: String = ""
) : JceStruct

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019-2022 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/dev/LICENSE
*/
package net.mamoe.mirai.internal.network.protocol.data.jce
import kotlinx.serialization.Serializable
import net.mamoe.mirai.internal.utils.io.JceStruct
import net.mamoe.mirai.internal.utils.io.serialization.tars.TarsId
import kotlin.jvm.JvmField
@Serializable
internal class SetGroupReq(
@JvmField @TarsId(0) val reqtype: Int = 0,
@JvmField @TarsId(1) val uin: Long = 0L,
@JvmField @TarsId(2) val vecBody: ByteArray? = null
) : JceStruct
@Serializable
internal class SetGroupResp(
@JvmField @TarsId(0) val reqtype: Byte = 0,
@JvmField @TarsId(1) val result: Byte = 0,
@JvmField @TarsId(2) val vecBody: ByteArray? = null,
@JvmField @TarsId(3) val errorString: String = ""
) : JceStruct

View File

@ -144,6 +144,8 @@ internal object KnownPacketFactories {
FriendList.DelFriend,
FriendList.GetTroopListSimplify,
FriendList.GetTroopMemberList,
FriendList.SetGroupReqPack,
FriendList.MoveGroupMemReqPack,
ImgStore.GroupPicUp,
PttStore.GroupPttUp,
PttStore.GroupPttDown,

View File

@ -20,11 +20,9 @@ import net.mamoe.mirai.internal.network.protocol.data.proto.Vec0xd50
import net.mamoe.mirai.internal.network.protocol.data.proto.Vec0xd6b
import net.mamoe.mirai.internal.network.protocol.packet.OutgoingPacketFactory
import net.mamoe.mirai.internal.network.protocol.packet.buildOutgoingUniPacket
import net.mamoe.mirai.internal.utils.io.serialization.jceRequestSBuffer
import net.mamoe.mirai.internal.utils.io.serialization.readUniPacket
import net.mamoe.mirai.internal.utils.io.serialization.toByteArray
import net.mamoe.mirai.internal.utils.io.serialization.writeJceStruct
import net.mamoe.mirai.internal.utils.io.serialization.*
import net.mamoe.mirai.utils.EMPTY_BYTE_ARRAY
import net.mamoe.mirai.utils.toByteArray
internal class FriendList {
@ -163,7 +161,10 @@ internal class FriendList {
class Response(
val selfInfo: FriendInfo?,
val totalFriendCount: Short,
val friendList: List<FriendInfo>
val friendList: List<FriendInfo>,
// for FriendGroup
val groupList: List<GroupInfo>,
val totoalGroupCount: Short
) : Packet {
override fun toString(): String = "FriendList.GetFriendGroupList.Response"
}
@ -177,7 +178,9 @@ internal class FriendList {
return Response(
res.stSelfInfo,
res.totoalFriendCount,
res.vecFriendInfo.orEmpty()
res.vecFriendInfo.orEmpty(),
res.vecGroupInfo.orEmpty(),
res.totoalGroupCount ?: -1
)
}
@ -247,7 +250,7 @@ internal class FriendList {
GetFriendListReq.serializer(),
GetFriendListReq(
reqtype = 3,
ifReflush = if (friendListStartIndex <= 0) {
ifReflush = if (friendListStartIndex + groupListStartIndex <= 0) {
0
} else {
1
@ -285,4 +288,118 @@ internal class FriendList {
)
}
}
// for FriendGroup
internal object SetGroupReqPack : OutgoingPacketFactory<SetGroupReqPack.Response>("friendlist.SetGroupReq") {
class Response(
// Success: result == 0x00
val result: Byte,
val errStr: String,
// groupId for delete
val groupId: Int,
val isSuccess: Boolean = result.toInt() == 0
) : Packet {
override fun toString(): String {
return "SetGroupResp(isSuccess=$isSuccess,resultCode=$result, errString=$errStr, groupId=$groupId)"
}
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val pack = this.readUniPacket(SetGroupResp.serializer())
return if (pack.result.toInt() == 0) {
Response(pack.result, pack.errorString, pack.vecBody?.get(8)?.toInt() ?: -1)
} else {
Response(pack.result, pack.errorString, -1)
}
}
object New {
operator fun invoke(
client: QQAndroidClient, groupName: String
) = buildOutgoingUniPacket(client) {
val arr = groupName.toByteArray()
// maybe is constant
val constant: Byte = 0x01
writeJceRequestPacket(
servantName = "mqq.IMService.FriendListServiceServantObj",
funcName = "SetGroupReq",
serializer = SetGroupReq.serializer(),
body = SetGroupReq(0, client.uin, byteArrayOf(constant, arr.size.toByte()) + arr)
)
}
}
object Rename {
operator fun invoke(client: QQAndroidClient, newName: String, id: Int) = buildOutgoingUniPacket(client) {
val arr = newName.toByteArray()
writeJceRequestPacket(
servantName = "mqq.IMService.FriendListServiceServantObj",
funcName = "SetGroupReq",
serializer = SetGroupReq.serializer(),
body = SetGroupReq(1, client.uin, byteArrayOf(id.toByte(), arr.size.toByte()) + arr)
)
}
}
object Delete {
operator fun invoke(client: QQAndroidClient, id: Int) = buildOutgoingUniPacket(client) {
writeJceRequestPacket(
servantName = "mqq.IMService.FriendListServiceServantObj",
funcName = "SetGroupReq",
serializer = SetGroupReq.serializer(),
body = SetGroupReq(2, client.uin, byteArrayOf(id.toByte()))
)
}
}
}
// for FriendGroup
internal object MoveGroupMemReqPack :
OutgoingPacketFactory<MoveGroupMemReqPack.Response>("friendlist.MovGroupMemReq") {
private fun Long.toByteArray2(): ByteArray {
val arr = this.toByteArray()
val index = arr.indexOfFirst { it.toInt() != 0 }
return arr.sliceArray(index until arr.size)
}
// 如果不成功会自动移动到id = 0的默认好友分组, result 还是会返回0
class Response(
// Success: result == 0x00
val result: Byte,
val errStr: String,
val isSuccess: Boolean = result.toInt() == 0
) : Packet {
override fun toString(): String {
return "MoveGroupMemReq(isSuccess=$isSuccess,resultCode=$result, errString=$errStr)"
}
}
override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response {
val pack = this.readUniPacket(MovGroupMemResp.serializer())
return Response(pack.result, pack.errorString)
}
operator fun invoke(
client: QQAndroidClient,
// friend id
id: Long,
// friend group id
groupId: Int
) = buildOutgoingUniPacket(client) {
writeJceRequestPacket(
servantName = "mqq.IMService.FriendListServiceServantObj",
funcName = "MovGroupMemReq",
serializer = MovGroupMemReq.serializer(),
body = MovGroupMemReq(
client.uin,
0,
byteArrayOf(
0x01,
0x00,
(id.toByteArray2().size + 1).toByte()
) + id.toByteArray2() + byteArrayOf(groupId.toByte(), 0x00, 0x00)
)
)
}
}
}

View File

@ -35,9 +35,10 @@ internal data class UserProfileImpl(
override val qLevel: Int,
override val sex: UserProfile.Sex,
override val sign: String,
override val friendGroupId: Int,
) : Packet, UserProfile {
override fun toString(): String {
return "UserProfile(nickname=$nickname, email=$email, age=$age, qLevel=$qLevel, sex=$sex, sign=$sign)"
return "UserProfile(nickname=$nickname, email=$email, age=$age, qLevel=$qLevel, sex=$sex, sign=$sign, friendGroupId=$friendGroupId)"
}
}
@ -118,7 +119,8 @@ internal object SummaryCard {
1 -> UserProfile.Sex.FEMALE
else -> UserProfile.Sex.UNKNOWN
},
sign = sign
sign = sign,
friendGroupId = response.uFriendGroupId?.toInt() ?: 0
)
}
}

View File

@ -120,8 +120,8 @@ internal interface GroupExtensions {
friends.delegate.add(friend)
}
fun Bot.addFriend(id: Long, nick: String = "friend$id", remark: String = ""): FriendImpl {
return FriendImpl(bot.cast(), bot.coroutineContext, FriendInfoImpl(id, nick, remark)).also {
fun Bot.addFriend(id: Long, nick: String = "friend$id", remark: String = "", friendGroupId: Int = 0): FriendImpl {
return FriendImpl(bot.cast(), bot.coroutineContext, FriendInfoImpl(id, nick, remark, friendGroupId)).also {
friends.delegate.add(it)
}
}