From 0b01fd7b3634e4bd1b5bc1e789baa99061cd0fe2 Mon Sep 17 00:00:00 2001 From: "jiahua.liu" <n@mamoe.net> Date: Fri, 31 Jan 2020 18:06:23 +0800 Subject: [PATCH 1/2] troop List --- .../network/QQAndroidBotNetworkHandler.kt | 11 ++- .../qqandroid/network/http/HttpRequest.kt | 81 +++++++++++++++++++ .../network/protocol/packet/PacketFactory.kt | 2 +- .../protocol/packet/list/FriendListPacket.kt | 7 +- .../androidPacketTests/clientToServer.kt | 3 + .../androidPacketTests/serverToClient.kt | 3 + 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/http/HttpRequest.kt diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index 8bb5551f5..bca5c7436 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -1,5 +1,6 @@ package net.mamoe.mirai.qqandroid.network +import io.ktor.client.HttpClient import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic import kotlinx.coroutines.* @@ -98,6 +99,7 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler suspend fun doInit() { //start updating friend/group list bot.logger.info("Start updating friend/group list") + /* val data = FriendList.GetFriendGroupList( bot.client, @@ -106,11 +108,14 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler 0, 2 ).sendAndExpect<FriendList.GetFriendGroupList.Response>() - */ - val data = FriendList.GetTroopList( + */ + + val data = FriendList.GetTroopListSimplify( bot.client - ).sendAndExpect<FriendList.GetFriendGroupList.Response>() + ).sendAndExpect<FriendList.GetTroopListSimplify.Response>(100000) println(data.contentToString()) + + } doLogin() diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/http/HttpRequest.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/http/HttpRequest.kt new file mode 100644 index 000000000..b74ac1501 --- /dev/null +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/http/HttpRequest.kt @@ -0,0 +1,81 @@ +package net.mamoe.mirai.qqandroid.network.http + +import io.ktor.client.HttpClient +import io.ktor.client.request.* +import io.ktor.client.response.HttpResponse +import io.ktor.http.ContentType +import io.ktor.http.URLProtocol +import io.ktor.http.setCookie +import io.ktor.http.userAgent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.io.readRemaining +import kotlinx.coroutines.withContext +import net.mamoe.mirai.qqandroid.QQAndroidBot +import net.mamoe.mirai.qqandroid.network.QQAndroidClient +import net.mamoe.mirai.utils.cryptor.contentToString +import net.mamoe.mirai.utils.currentTimeMillis +import net.mamoe.mirai.utils.io.readRemainingBytes +import net.mamoe.mirai.utils.io.toUHexString + +/** + * 好像不需要了 + */ +object HttpRequest { + private lateinit var cookie: String +} + + +internal suspend fun HttpClient.getPTLoginCookies( + client: QQAndroidClient +): String { + //$"https://ssl.ptlogin2.qq.com/jump?pt_clientver=5593&pt_src=1&keyindex=9&ptlang=2052&clientuin={QQ}&clientkey={Util.ToHex(TXProtocol.BufServiceTicketHttp, "", "{0}")}&u1=https:%2F%2Fuser.qzone.qq.com%2F417085811%3FADUIN=417085811%26ADSESSION={Util.GetTimeMillis(DateTime.Now)}%26ADTAG=CLIENT.QQ.5593_MyTip.0%26ADPUBNO=26841&source=namecardhoverstar" + // "https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin={0}&clientkey={1}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441", + val res = post<HttpResponse> { + println(client.wLoginSigInfo.userStWebSig.data.toUHexString().replace(" ", "").toLowerCase()) + url { + protocol = URLProtocol.HTTPS + host = "ssl.ptlogin2.qq.com" + path( + "/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin=${client.uin}&clientkey=${client.wLoginSigInfo.userStWebSig.data.toUHexString().replace( + " ", + "" + )}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441&FADUIN=417085811&ADSESSION=${currentTimeMillis}&source=namecardhoverstar" + ) + } + headers { + userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36") + } + } + + println(res.status) + println(res.setCookie()) + println(res.content.readRemaining().readRemainingBytes().toUHexString()) + return "done"; +} + + +internal suspend fun HttpClient.getGroupList( + client: QQAndroidClient +): String { + // "https://ssl.ptlogin2.qq.com/jump?pt_clientver=5509&pt_src=1&keyindex=9&clientuin={0}&clientkey={1}&u1=http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441", + val res = get<HttpResponse> { + url { + protocol = URLProtocol.HTTPS + host = "ssl.ptlogin2.qq.com" + path("jump") + parameters["pt_clientver"] = "5509" + parameters["pt_src"] = "1" + parameters["keyindex"] = "9" + parameters["u1"] = "http%3A%2F%2Fqun.qq.com%2Fmember.html%23gid%3D168209441" + parameters["clientuin"] = client.uin.toString() + parameters["clientkey"] = client.wLoginSigInfo.userStWebSig.data.toUHexString() + } + headers { + userAgent("Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36") + } + } + + println(res.status) + println(res.setCookie()) + return "done"; +} diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt index a68e3778b..acf2834df 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt @@ -67,7 +67,7 @@ internal object KnownPacketFactories : List<PacketFactory<*>> by mutableListOf( MessageSvc.PushForceOffline, MessageSvc.PbSendMsg, FriendList.GetFriendGroupList, - FriendList.GetTroopList + FriendList.GetTroopListSimplify ) { // SvcReqMSFLoginNotify 自己的其他设备上限 // MessageSvc.PushReaded 电脑阅读了别人的消息, 告知手机 diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt index c62de6ef3..f3354ff3b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt @@ -15,7 +15,6 @@ import net.mamoe.mirai.qqandroid.network.protocol.packet.OutgoingPacket import net.mamoe.mirai.qqandroid.network.protocol.packet.PacketFactory import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket import net.mamoe.mirai.utils.cryptor.contentToString -import net.mamoe.mirai.utils.io.debugPrint import net.mamoe.mirai.utils.io.discardExact import net.mamoe.mirai.utils.io.readRemainingBytes import net.mamoe.mirai.utils.io.toUHexString @@ -23,8 +22,9 @@ import net.mamoe.mirai.utils.io.toUHexString internal class FriendList { - internal object GetTroopList : PacketFactory<GetTroopList.Response>("friendlist.GetTroopListReqV2") { - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GetTroopList.Response { + internal object GetTroopListSimplify : + PacketFactory<GetTroopListSimplify.Response>("friendlist.GetTroopListReqV2") { + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GetTroopListSimplify.Response { println("获取到了GetTroopList的回信") println(this.readRemainingBytes().toUHexString()) return Response() @@ -65,7 +65,6 @@ internal class FriendList { } } } - internal object GetFriendGroupList : PacketFactory<GetFriendGroupList.Response>("friendlist.getFriendGroupList") { class Response : Packet { diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt index 4bdaff37a..07958c3cd 100644 --- a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt +++ b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/clientToServer.kt @@ -276,6 +276,9 @@ fun ByteReadPacket.decodeUni() { //return readBytes(readInt() - 4).debugPrint("head").toReadPacket().apply { val commandName = readString(readInt() - 4).also { PacketLogger.warning("commandName=$it") } + if(commandName.contains("GetTroopList")){ + println("!\n".repeat(100)) + } println(commandName) println(" unknown4Bytes=" + readBytes(readInt() - 4).toUHexString()) // 00 00 00 1A 43 6F 6E 66 69 67 50 75 73 68 53 76 63 2E 50 75 73 68 52 65 73 70 diff --git a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/serverToClient.kt b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/serverToClient.kt index 77ca2dbfe..be316f9f5 100644 --- a/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/serverToClient.kt +++ b/mirai-core-qqandroid/src/jvmTest/kotlin/androidPacketTests/serverToClient.kt @@ -219,6 +219,9 @@ private fun parseSsoFrame(input: ByteReadPacket): KnownPacketFactories.IncomingP commandName = readString(readInt() - 4) DebugLogger.warning("commandName=$commandName") + if(commandName.contains("GetTroopList")){ + println("!\n".repeat(100)) + } val unknown = readBytes(readInt() - 4) //if (unknown.toInt() != 0x02B05B8B) DebugLogger.debug("got new unknown: ${unknown.toUHexString()}") From 192a83de0cf39ab306e2631be58e2ec8b6577068 Mon Sep 17 00:00:00 2001 From: "jiahua.liu" <n@mamoe.net> Date: Fri, 31 Jan 2020 21:17:53 +0800 Subject: [PATCH 2/2] troop List --- .../network/QQAndroidBotNetworkHandler.kt | 15 +++- .../network/protocol/data/jce/TroopList.kt | 81 +++++++++++++++++++ .../network/protocol/packet/PacketFactory.kt | 2 +- .../protocol/packet/list/FriendListPacket.kt | 44 +++++++--- 4 files changed, 125 insertions(+), 17 deletions(-) diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt index fb51a07b2..550414e09 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/QQAndroidBotNetworkHandler.kt @@ -102,19 +102,26 @@ internal class QQAndroidBotNetworkHandler(bot: QQAndroidBot) : BotNetworkHandler override suspend fun init() { //start updating friend/group list bot.logger.info("Start updating friend/group list") + /* val data = FriendList.GetFriendGroupList( bot.client, 0, - 1, + 20, 0, - 2 + 0 ).sendAndExpect<FriendList.GetFriendGroupList.Response>() - */ + + + println(data.contentToString()) + */ + val data = FriendList.GetTroopList( bot.client - ).sendAndExpect<FriendList.GetTroopList.Response>() + ).sendAndExpect<FriendList.GetTroopList.Response>(100000) println(data.contentToString()) + + } diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/TroopList.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/TroopList.kt index 296d4c133..63f465d9e 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/TroopList.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/data/jce/TroopList.kt @@ -24,3 +24,84 @@ internal class stTroopNumSimplify( @SerialId(2) val dwGroupFlagExt: Long? = null, @SerialId(3) val dwGroupRankSeq: Long? = null ) : JceStruct + + +@Serializable +internal class GetTroopListRespV2( + @SerialId(0) val uin: Long, + @SerialId(1) val troopcount: Short, + @SerialId(2) val result: Int, + @SerialId(3) val errorCode: Short? = null, + @SerialId(4) val vecCookies: ByteArray? = null, + @SerialId(5) val vecTroopList: List<stTroopNum>? = null, + @SerialId(6) val vecTroopListDel: List<stTroopNum>? = null, + @SerialId(7) val vecTroopRank: List<stGroupRankInfo>? = null, + @SerialId(8) val vecFavGroup: List<stFavoriteGroup>? = null, + @SerialId(9) val vecTroopListExt: List<stTroopNum>? = null +) : JceStruct + + +@Serializable +internal class stTroopNum( + @SerialId(0) val groupUin: Long, + @SerialId(1) val groupCode: Long, + @SerialId(2) val flag: Byte? = null, + @SerialId(3) val dwGroupInfoSeq: Long? = null, + @SerialId(4) val groupName: String? = "", + @SerialId(5) val groupMemo: String? = "", + @SerialId(6) val dwGroupFlagExt: Long? = null, + @SerialId(7) val dwGroupRankSeq: Long? = null, + @SerialId(8) val dwCertificationType: Long? = null, + @SerialId(9) val dwShutupTimestamp: Long? = null, + @SerialId(10) val dwMyShutupTimestamp: Long? = null, + @SerialId(11) val dwCmdUinUinFlag: Long? = null, + @SerialId(12) val dwAdditionalFlag: Long? = null, + @SerialId(13) val dwGroupTypeFlag: Long? = null, + @SerialId(14) val dwGroupSecType: Long? = null, + @SerialId(15) val dwGroupSecTypeInfo: Long? = null, + @SerialId(16) val dwGroupClassExt: Long? = null, + @SerialId(17) val dwAppPrivilegeFlag: Long? = null, + @SerialId(18) val dwSubscriptionUin: Long? = null, + @SerialId(19) val dwMemberNum: Long? = null, + @SerialId(20) val dwMemberNumSeq: Long? = null, + @SerialId(21) val dwMemberCardSeq: Long? = null, + @SerialId(22) val dwGroupFlagExt3: Long? = null, + @SerialId(23) val dwGroupOwnerUin: Long? = null, + @SerialId(24) val isConfGroup: Byte? = null, + @SerialId(25) val isModifyConfGroupFace: Byte? = null, + @SerialId(26) val isModifyConfGroupName: Byte? = null, + @SerialId(27) val dwCmduinJoinTime: Long? = null, + @SerialId(28) val ulCompanyId: Long? = null, + @SerialId(29) val dwMaxGroupMemberNum: Long? = null, + @SerialId(30) val dwCmdUinGroupMask: Long? = null, + @SerialId(31) val udwHLGuildAppid: Long? = null, + @SerialId(32) val udwHLGuildSubType: Long? = null, + @SerialId(33) val udwCmdUinRingtoneID: Long? = null, + @SerialId(34) val udwCmdUinFlagEx2: Long? = null +) : JceStruct + +@Serializable +internal class stGroupRankInfo( + @SerialId(0) val dwGroupCode: Long, + @SerialId(1) val groupRankSysFlag: Byte? = null, + @SerialId(2) val groupRankUserFlag: Byte? = null, + @SerialId(3) val vecRankMap: List<stLevelRankPair>? = null, + @SerialId(4) val dwGroupRankSeq: Long? = null, + @SerialId(5) val ownerName: String? = "", + @SerialId(6) val adminName: String? = "", + @SerialId(7) val dwOfficeMode: Long? = null +) : JceStruct + +@Serializable +internal class stFavoriteGroup( + @SerialId(0) val dwGroupCode: Long, + @SerialId(1) val dwTimestamp: Long? = null, + @SerialId(2) val dwSnsFlag: Long? = 1L, + @SerialId(3) val dwOpenTimestamp: Long? = null +) : JceStruct + +@Serializable +internal class stLevelRankPair( + @SerialId(0) val dwLevel: Long? = null, + @SerialId(1) val rank: String? = "" +) : JceStruct diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt index acf2834df..a68e3778b 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/PacketFactory.kt @@ -67,7 +67,7 @@ internal object KnownPacketFactories : List<PacketFactory<*>> by mutableListOf( MessageSvc.PushForceOffline, MessageSvc.PbSendMsg, FriendList.GetFriendGroupList, - FriendList.GetTroopListSimplify + FriendList.GetTroopList ) { // SvcReqMSFLoginNotify 自己的其他设备上限 // MessageSvc.PushReaded 电脑阅读了别人的消息, 告知手机 diff --git a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt index 6369b5642..43f8ab69c 100644 --- a/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt +++ b/mirai-core-qqandroid/src/commonMain/kotlin/net/mamoe/mirai/qqandroid/network/protocol/packet/list/FriendListPacket.kt @@ -8,16 +8,20 @@ import net.mamoe.mirai.qqandroid.io.serialization.jceRequestSBuffer import net.mamoe.mirai.qqandroid.io.serialization.toByteArray import net.mamoe.mirai.qqandroid.io.serialization.writeJceStruct import net.mamoe.mirai.qqandroid.network.QQAndroidClient +import net.mamoe.mirai.qqandroid.network.protocol.data.jce.* import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListReq import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetFriendListResp import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GetTroopListReqV2Simplify +import net.mamoe.mirai.qqandroid.network.protocol.data.jce.GroupInfo import net.mamoe.mirai.qqandroid.network.protocol.data.jce.RequestPacket import net.mamoe.mirai.qqandroid.network.protocol.data.proto.Vec0xd50 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.PacketFactory import net.mamoe.mirai.qqandroid.network.protocol.packet.buildOutgoingUniPacket +import net.mamoe.mirai.qqandroid.network.protocol.packet.list.FriendList.GetFriendGroupList.decode import net.mamoe.mirai.utils.cryptor.contentToString +import net.mamoe.mirai.utils.io.debugPrint import net.mamoe.mirai.utils.io.discardExact import net.mamoe.mirai.utils.io.readRemainingBytes import net.mamoe.mirai.utils.io.toUHexString @@ -25,15 +29,28 @@ import net.mamoe.mirai.utils.io.toUHexString internal class FriendList { - internal object GetTroopListSimplify : - PacketFactory<GetTroopListSimplify.Response>("friendlist.GetTroopListReqV2") { - override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GetTroopListSimplify.Response { - println("获取到了GetTroopList的回信") - println(this.readRemainingBytes().toUHexString()) - return Response() + /** + * Get Troop List不一定会得到服务器的回应 据推测与群数量有关 + * 因此 应对timeout方法做出处理 + * timeout时间应不小于 8s? + * + */ + internal object GetTroopList : + PacketFactory<GetTroopList.Response>("friendlist.GetTroopListReqV2") { + override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): GetTroopList.Response { + debugPrint() + this.discardExact(4) + val res = this.decodeUniPacket(GetTroopListRespV2.serializer()) + println(res.contentToString()) + return Response( + + ) + } - class Response : Packet { + class Response( + + ) : Packet { override fun toString(): String = "FriendList.GetFriendGroupList.Response" } @@ -49,7 +66,6 @@ internal class FriendList { iVersion = 3, cPacketType = 0x00, iMessageType = 0x00000, - iRequestId = 1921334513, sBuffer = jceRequestSBuffer( "GetTroopListReqV2Simplify", GetTroopListReqV2Simplify.serializer(), @@ -69,16 +85,20 @@ internal class FriendList { } } internal object GetFriendGroupList : PacketFactory<GetFriendGroupList.Response>("friendlist.getFriendGroupList") { - - class Response : Packet { + class Response( + val totalFriendCount: Short, + val friendList: List<FriendInfo> + ) : Packet { override fun toString(): String = "FriendList.GetFriendGroupList.Response" } override suspend fun ByteReadPacket.decode(bot: QQAndroidBot): Response { this.discardExact(4) val res = this.decodeUniPacket(GetFriendListResp.serializer()) - println(res.contentToString()) - return Response() + return Response( + res.totoalFriendCount, + res.vecFriendInfo.orEmpty() + ) } operator fun invoke(