From ca47f9dbdfa5c82744c89c268c730c028c7e8aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E6=B1=A0?= Date: Tue, 12 Mar 2024 18:46:05 +0800 Subject: [PATCH] =?UTF-8?q?`Shamrock`:=20=E3=82=B0=E3=83=AB=E3=83=BC?= =?UTF-8?q?=E3=83=97=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=B5=E3=83=BC?= =?UTF-8?q?=E3=83=93=E3=82=B9=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 白池 --- kritor | 2 +- .../main/java/kritor/server/KritorServer.kt | 3 + .../java/kritor/service/ContactService.kt | 31 +- .../main/java/kritor/service/FriendService.kt | 41 ++ .../java/kritor/service/GroupFileService.kt | 138 ++++ .../main/java/kritor/service/GroupService.kt | 406 ++++++++++++ .../main/java/kritor/service/KritorService.kt | 7 +- .../moe/fuqiuluo/shamrock/tools/Kotlinx.kt | 4 +- .../src/main/java/qq/service/QQInterfaces.kt | 120 +++- .../java/qq/service/contact/ContactHelper.kt | 95 ++- .../java/qq/service/file/GroupFileHelper.kt | 162 +++++ .../java/qq/service/friend/FriendHelper.kt | 40 ++ .../main/java/qq/service/group/GroupHelper.kt | 617 ++++++++++++++++++ .../qq/service/group/NotJoinedGroupInfo.kt | 17 + .../qq/service/group/ProhibitedMemberInfo.kt | 10 + .../java/qq/service/internals/MSFHandler.kt | 14 +- 16 files changed, 1693 insertions(+), 14 deletions(-) create mode 100644 xposed/src/main/java/kritor/service/FriendService.kt create mode 100644 xposed/src/main/java/kritor/service/GroupFileService.kt create mode 100644 xposed/src/main/java/kritor/service/GroupService.kt create mode 100644 xposed/src/main/java/qq/service/file/GroupFileHelper.kt create mode 100644 xposed/src/main/java/qq/service/friend/FriendHelper.kt create mode 100644 xposed/src/main/java/qq/service/group/GroupHelper.kt create mode 100644 xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt create mode 100644 xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt diff --git a/kritor b/kritor index 201e91e..f007631 160000 --- a/kritor +++ b/kritor @@ -1 +1 @@ -Subproject commit 201e91e73225ce3f1ec098c06a5cf6a717d913e5 +Subproject commit f007631c7e17fe6220055a404fdaaa6e7a24a7ef diff --git a/xposed/src/main/java/kritor/server/KritorServer.kt b/xposed/src/main/java/kritor/server/KritorServer.kt index df27d30..f7fa4c1 100644 --- a/xposed/src/main/java/kritor/server/KritorServer.kt +++ b/xposed/src/main/java/kritor/server/KritorServer.kt @@ -21,6 +21,9 @@ class KritorServer( .addService(Authentication) .addService(ContactService) .addService(KritorService) + .addService(FriendService) + .addService(GroupService) + .addService(GroupFileService) .build()!! fun start(block: Boolean = false) { diff --git a/xposed/src/main/java/kritor/service/ContactService.kt b/xposed/src/main/java/kritor/service/ContactService.kt index e973a65..267fd49 100644 --- a/xposed/src/main/java/kritor/service/ContactService.kt +++ b/xposed/src/main/java/kritor/service/ContactService.kt @@ -21,8 +21,11 @@ import io.kritor.contact.SetProfileCardResponse import io.kritor.contact.StrangerExt import io.kritor.contact.StrangerInfo import io.kritor.contact.StrangerInfoRequest +import io.kritor.contact.VoteUserRequest +import io.kritor.contact.VoteUserResponse import io.kritor.contact.profileCard import io.kritor.contact.strangerInfo +import io.kritor.contact.voteUserResponse import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import qq.service.QQInterfaces @@ -31,10 +34,32 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { + @Grpc("ContactService", "VoteUser") + override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse { + ContactHelper.voteUser(when(request.accountCase!!) { + VoteUserRequest.AccountCase.ACCOUNT_UIN -> request.accountUin + VoteUserRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong() + VoteUserRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("account not set") + ) + }, request.voteCount).onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withDescription(it.stackTraceToString()) + ) + } + return voteUserResponse { } + } + @Grpc("ContactService", "GetProfileCard") override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard { - val uin = if (request.hasUin()) request.uin - else ContactHelper.getUinByUidAsync(request.uid).toLong() + val uin = when (request.accountCase!!) { + ProfileCardRequest.AccountCase.ACCOUNT_UIN -> request.accountUin + ProfileCardRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong() + ProfileCardRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("account not set") + ) + } + val contact = ContactHelper.getProfileCard(uin) contact.onFailure { @@ -46,7 +71,7 @@ object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() { contact.onSuccess { return profileCard { this.uin = it.uin.toLong() - this.uid = if (request.hasUid()) request.uid + this.uid = if (request.hasAccountUid()) request.accountUid else ContactHelper.getUidByUinAsync(it.uin.toLong()) this.name = it.strNick ?: "" this.remark = it.strReMark ?: "" diff --git a/xposed/src/main/java/kritor/service/FriendService.kt b/xposed/src/main/java/kritor/service/FriendService.kt new file mode 100644 index 0000000..6a2c862 --- /dev/null +++ b/xposed/src/main/java/kritor/service/FriendService.kt @@ -0,0 +1,41 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.friend.FriendServiceGrpcKt +import io.kritor.friend.GetFriendListRequest +import io.kritor.friend.GetFriendListResponse +import io.kritor.friend.friendData +import io.kritor.friend.friendExt +import io.kritor.friend.getFriendListResponse +import qq.service.contact.ContactHelper +import qq.service.friend.FriendHelper + +object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() { + @Grpc("FriendService", "GetFriendList") + override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse { + val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL + .withDescription(it.stackTraceToString()) + ) + }.getOrThrow() + + return getFriendListResponse { + friendList.forEach { + this.friendList.add(friendData { + uin = it.uin.toLong() + uid = ContactHelper.getUidByUinAsync(uin) + qid = "" + nick = it.name ?: "" + remark = it.remark ?: "" + age = it.age + level = 0 + gender = it.gender.toInt() + groupId = it.groupid + ext = friendExt {}.toByteString() + }) + } + } + } + +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt new file mode 100644 index 0000000..3dc2121 --- /dev/null +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -0,0 +1,138 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.file.* +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.symbols.decodeProtobuf +import protobuf.auto.toByteArray +import protobuf.oidb.cmd0x6d7.CreateFolderReq +import protobuf.oidb.cmd0x6d7.DeleteFolderReq +import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody +import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody +import protobuf.oidb.cmd0x6d7.RenameFolderReq +import qq.service.QQInterfaces +import qq.service.file.GroupFileHelper +import qq.service.file.GroupFileHelper.getGroupFileSystemInfo +import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 +import tencent.im.oidb.cmd0x6d8.oidb_0x6d8 +import tencent.im.oidb.oidb_sso + +internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() { + @Grpc("GroupFileService", "CreateFolder") + override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse { + val data = Oidb0x6d7ReqBody( + createFolder = CreateFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + parentFolderId = "/", + folderName = request.name + ) + ).toByteArray() + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get() + .toByteArray() + .decodeProtobuf() + if (rsp.createFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}")) + } + return createFolderResponse { + this.id = rsp.createFolder?.folderInfo?.folderId ?: "" + this.usedSpace = 0 + } + } + + @Grpc("GroupFileService", "DeleteFolder") + override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse { + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody( + deleteFolder = DeleteFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + folderId = request.folderId + ) + ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() + if (rsp.deleteFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}")) + } + return deleteFolderResponse { } + } + + @Grpc("GroupFileService", "DeleteFile") + override suspend fun deleteFile(request: DeleteFileRequest): DeleteFileResponse { + val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply { + delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply { + uint64_group_code.set(request.groupId) + uint32_app_id.set(3) + uint32_bus_id.set(request.busId) + str_parent_folder_id.set("/") + str_file_id.set(request.fileId) + }) + } + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray()) + ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidb_0x6d6.RspBody().apply { + mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray()) + } + if (rsp.delete_file_rsp.int32_ret_code.get() != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}")) + } + return deleteFileResponse { } + } + + @Grpc("GroupFileService", "RenameFolder") + override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse { + val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody( + renameFolder = RenameFolderReq( + groupCode = request.groupId.toULong(), + appId = 3u, + folderId = request.folderId, + folderName = request.name + ) + ).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val oidbPkg = oidb_sso.OIDBSSOPkg() + oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf() + if (rsp.renameFolder?.retCode != 0) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}")) + } + return renameFolderResponse { } + } + + override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse { + return getGroupFileSystemInfo(request.groupId) + } + + @Grpc("GroupFileService", "GetRootFiles") + override suspend fun getRootFiles(request: GetRootFilesRequest): GetRootFilesResponse { + return getRootFilesResponse { + val response = GroupFileHelper.getGroupFiles(request.groupId) + this.files.addAll(response.filesList) + this.folders.addAll(response.foldersList) + } + } + + @Grpc("GroupFileService", "GetFiles") + override suspend fun getFiles(request: GetFilesRequest): GetFilesResponse { + return GroupFileHelper.getGroupFiles(request.groupId, request.folderId) + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/GroupService.kt b/xposed/src/main/java/kritor/service/GroupService.kt new file mode 100644 index 0000000..8e0d5b8 --- /dev/null +++ b/xposed/src/main/java/kritor/service/GroupService.kt @@ -0,0 +1,406 @@ +package kritor.service + +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.group.BanMemberRequest +import io.kritor.group.BanMemberResponse +import io.kritor.group.GetGroupHonorRequest +import io.kritor.group.GetGroupHonorResponse +import io.kritor.group.GetGroupInfoRequest +import io.kritor.group.GetGroupInfoResponse +import io.kritor.group.GetGroupListRequest +import io.kritor.group.GetGroupListResponse +import io.kritor.group.GetGroupMemberInfoRequest +import io.kritor.group.GetGroupMemberInfoResponse +import io.kritor.group.GetGroupMemberListRequest +import io.kritor.group.GetGroupMemberListResponse +import io.kritor.group.GetNotJoinedGroupInfoRequest +import io.kritor.group.GetNotJoinedGroupInfoResponse +import io.kritor.group.GetProhibitedUserListRequest +import io.kritor.group.GetProhibitedUserListResponse +import io.kritor.group.GetRemainCountAtAllRequest +import io.kritor.group.GetRemainCountAtAllResponse +import io.kritor.group.GroupServiceGrpcKt +import io.kritor.group.KickMemberRequest +import io.kritor.group.KickMemberResponse +import io.kritor.group.LeaveGroupRequest +import io.kritor.group.LeaveGroupResponse +import io.kritor.group.ModifyGroupNameRequest +import io.kritor.group.ModifyGroupNameResponse +import io.kritor.group.ModifyGroupRemarkRequest +import io.kritor.group.ModifyGroupRemarkResponse +import io.kritor.group.ModifyMemberCardRequest +import io.kritor.group.ModifyMemberCardResponse +import io.kritor.group.PokeMemberRequest +import io.kritor.group.PokeMemberResponse +import io.kritor.group.SetGroupAdminRequest +import io.kritor.group.SetGroupAdminResponse +import io.kritor.group.SetGroupUniqueTitleRequest +import io.kritor.group.SetGroupUniqueTitleResponse +import io.kritor.group.SetGroupWholeBanRequest +import io.kritor.group.SetGroupWholeBanResponse +import io.kritor.group.banMemberResponse +import io.kritor.group.getGroupHonorResponse +import io.kritor.group.getGroupInfoResponse +import io.kritor.group.getGroupListResponse +import io.kritor.group.getGroupMemberInfoResponse +import io.kritor.group.getGroupMemberListResponse +import io.kritor.group.getNotJoinedGroupInfoResponse +import io.kritor.group.getProhibitedUserListResponse +import io.kritor.group.getRemainCountAtAllResponse +import io.kritor.group.groupHonorInfo +import io.kritor.group.groupMemberInfo +import io.kritor.group.kickMemberResponse +import io.kritor.group.leaveGroupResponse +import io.kritor.group.modifyGroupNameResponse +import io.kritor.group.modifyGroupRemarkResponse +import io.kritor.group.modifyMemberCardResponse +import io.kritor.group.notJoinedGroupInfo +import io.kritor.group.pokeMemberResponse +import io.kritor.group.prohibitedUserInfo +import io.kritor.group.setGroupAdminResponse +import io.kritor.group.setGroupUniqueTitleResponse +import io.kritor.group.setGroupWholeBanResponse +import moe.fuqiuluo.shamrock.helper.TroopHonorHelper +import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import qq.service.contact.ContactHelper +import qq.service.group.GroupHelper + +internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() { + @Grpc("GroupService", "BanMember") + override suspend fun banMember(request: BanMemberRequest): BanMemberResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.banMember(request.groupId, when(request.targetCase!!) { + BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.duration) + + return banMemberResponse { + groupId = request.groupId + } + } + + @Grpc("GroupService", "PokeMember") + override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse { + GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) { + PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }) + return pokeMemberResponse { } + } + + @Grpc("GroupService", "KickMember") + override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + GroupHelper.kickMember(request.groupId, request.rejectAddRequest, if (request.hasKickReason()) request.kickReason else "", when(request.targetCase!!) { + KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin + KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }) + return kickMemberResponse { } + } + + @Grpc("GroupService", "LeaveGroup") + override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse { + GroupHelper.resignTroop(request.groupId.toString()) + return leaveGroupResponse { } + } + + @Grpc("GroupService", "ModifyMemberCard") + override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) { + ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin + ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.card) + return modifyMemberCardResponse { } + } + + @Grpc("GroupService", "ModifyGroupName") + override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName) + + return modifyGroupNameResponse { } + } + + @Grpc("GroupService", "ModifyGroupRemark") + override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse { + GroupHelper.modifyGroupRemark(request.groupId, request.remark) + + return modifyGroupRemarkResponse { } + } + + @Grpc("GroupService", "SetGroupAdmin") + override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse { + if (!GroupHelper.isOwner(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) { + SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin + SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.isAdmin) + + return setGroupAdminResponse { } + } + + @Grpc("GroupService", "SetGroupUniqueTitle") + override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) { + SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin + SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }, request.uniqueTitle) + + return setGroupUniqueTitleResponse { } + } + + @Grpc("GroupService", "SetGroupWholeBan") + override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse { + if (!GroupHelper.isAdmin(request.groupId.toString())) { + throw StatusRuntimeException(Status.PERMISSION_DENIED + .withDescription("You are not admin of this group") + ) + } + + GroupHelper.setGroupWholeBan(request.groupId, request.isBan) + return setGroupWholeBanResponse { } + } + + @Grpc("GroupService", "GetGroupInfo") + override suspend fun getGroupInfo(request: GetGroupInfoRequest): GetGroupInfoResponse { + val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it)) + }.getOrThrow() + return getGroupInfoResponse { + this.groupInfo = io.kritor.group.groupInfo { + groupId = groupInfo.troopcode.toLong() + groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: "" + groupRemark = groupInfo.troopRemark ?: "" + owner = groupInfo.troopowneruin?.toLong() ?: 0 + admins.addAll(GroupHelper.getAdminList(groupId)) + maxMemberCount = groupInfo.wMemberMax + memberCount = groupInfo.wMemberNum + groupUin = groupInfo.troopuin?.toLong() ?: 0 + } + } + } + + @Grpc("GroupService", "GetGroupList") + override suspend fun getGroupList(request: GetGroupListRequest): GetGroupListResponse { + val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it)) + }.getOrThrow() + return getGroupListResponse { + groupList.forEach { groupInfo -> + this.groupInfo.add(io.kritor.group.groupInfo { + groupId = groupInfo.troopcode.toLong() + groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: "" + groupRemark = groupInfo.troopRemark ?: "" + owner = groupInfo.troopowneruin?.toLong() ?: 0 + admins.addAll(GroupHelper.getAdminList(groupId)) + maxMemberCount = groupInfo.wMemberMax + memberCount = groupInfo.wMemberNum + groupUin = groupInfo.troopuin?.toLong() ?: 0 + }) + } + } + } + + @Grpc("GroupService", "GetGroupMemberInfo") + override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse { + val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, when(request.targetCase!!) { + GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin + GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong() + else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT + .withDescription("target not set") + ) + }).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it)) + }.getOrThrow() + return getGroupMemberInfoResponse { + groupMemberInfo = groupMemberInfo { + uid = if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.UID) request.uid else ContactHelper.getUidByUinAsync(request.uin) + uin = memberInfo.memberuin?.toLong() ?: 0 + nick = memberInfo.troopnick + .ifNullOrEmpty { memberInfo.hwName } + .ifNullOrEmpty { memberInfo.troopColorNick } + .ifNullOrEmpty { memberInfo.friendnick } ?: "" + age = memberInfo.age.toInt() + uniqueTitle = memberInfo.mUniqueTitle ?: "" + uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire + card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: "" + joinTime = memberInfo.join_time + lastActiveTime = memberInfo.last_active_time + level = memberInfo.level + shutUpTimestamp = memberInfo.gagTimeStamp + + distance = memberInfo.distance + honor.addAll((memberInfo.honorList ?: "") + .split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }) + unfriendly = false + cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) + } + } + } + + @Grpc("GroupService", "GetGroupMemberList") + override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse { + val memberList = GroupHelper.getGroupMemberList(request.groupId.toString(), if (request.hasRefresh()) request.refresh else false).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it)) + }.getOrThrow() + return getGroupMemberListResponse { + memberList.forEach { memberInfo -> + this.groupMemberInfo.add(groupMemberInfo { + uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0) + uin = memberInfo.memberuin?.toLong() ?: 0 + nick = memberInfo.troopnick + .ifNullOrEmpty { memberInfo.hwName } + .ifNullOrEmpty { memberInfo.troopColorNick } + .ifNullOrEmpty { memberInfo.friendnick } ?: "" + age = memberInfo.age.toInt() + uniqueTitle = memberInfo.mUniqueTitle ?: "" + uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire + card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: "" + joinTime = memberInfo.join_time + lastActiveTime = memberInfo.last_active_time + level = memberInfo.level + shutUpTimestamp = memberInfo.gagTimeStamp + + distance = memberInfo.distance + honor.addAll((memberInfo.honorList ?: "") + .split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }) + unfriendly = false + cardChangeable = GroupHelper.isAdmin(request.groupId.toString()) + }) + } + } + } + + @Grpc("GroupService", "GetProhibitedUserList") + override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse { + val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)) + }.getOrThrow() + return getProhibitedUserListResponse { + prohibitedList.forEach { + this.prohibitedUserInfo.add(prohibitedUserInfo { + uid = ContactHelper.getUidByUinAsync(it.memberUin) + uin = it.memberUin + prohibitedTime = it.shutuptimestap + }) + } + } + } + + @Grpc("GroupService", "GetRemainCountAtAll") + override suspend fun getRemainCountAtAll(request: GetRemainCountAtAllRequest): GetRemainCountAtAllResponse { + val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it)) + }.getOrThrow() + return getRemainCountAtAllResponse { + accessAtAll = remainAtAllRsp.bool_can_at_all.get() + remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get() + remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get() + } + } + + @Grpc("GroupService", "GetNotJoinedGroupInfo") + override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse { + val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)) + }.getOrThrow() + return getNotJoinedGroupInfoResponse { + this.groupInfo = notJoinedGroupInfo { + groupId = groupInfo.groupId + groupName = groupInfo.groupName + owner = groupInfo.owner + maxMemberCount = groupInfo.maxMember + memberCount = groupInfo.memberCount + groupDesc = groupInfo.groupDesc + createTime = groupInfo.createTime.toInt() + groupFlag = groupInfo.groupFlag + groupFlagExt = groupInfo.groupFlagExt + } + } + } + + @Grpc("GroupService", "GetGroupHonor") + override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse { + return getGroupHonorResponse { + GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it)) + }.onSuccess { memberList -> + memberList.forEach { member -> + (member.honorList ?: "").split("|") + .filter { it.isNotBlank() } + .map { it.toInt() }.forEach { + val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag) + if (honor != null) { + groupHonorInfo.add(groupHonorInfo { + uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong()) + uin = member.memberuin.toLong() + nick = member.troopnick + .ifNullOrEmpty { member.hwName } + .ifNullOrEmpty { member.troopColorNick } + .ifNullOrEmpty { member.friendnick } ?: "" + honorName = honor.honorName + avatar = honor.honorIconUrl + id = honor.honorId + description = honor.honorUrl + }) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/kritor/service/KritorService.kt b/xposed/src/main/java/kritor/service/KritorService.kt index e53387b..45eb251 100644 --- a/xposed/src/main/java/kritor/service/KritorService.kt +++ b/xposed/src/main/java/kritor/service/KritorService.kt @@ -104,8 +104,11 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() { @Grpc("KritorService", "SwitchAccount") override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse { - val uin = if (request.hasAccountUin()) request.accountUin.toString() - else ContactHelper.getUinByUidAsync(request.accountUid) + val uin = when(request.accountCase!!) { + SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid) + SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString() + SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("account not found")) + } val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin } ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found")) runCatching { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt index cb40366..2f5c3ce 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/tools/Kotlinx.kt @@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray { .let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s } } ?: "null" -fun String?.ifNullOrEmpty(defaultValue: String?): String? { - return if (this.isNullOrEmpty()) defaultValue else this +fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? { + return if (this.isNullOrEmpty()) defaultValue() else this } @JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray { diff --git a/xposed/src/main/java/qq/service/QQInterfaces.kt b/xposed/src/main/java/qq/service/QQInterfaces.kt index b37a104..2e6b047 100644 --- a/xposed/src/main/java/qq/service/QQInterfaces.kt +++ b/xposed/src/main/java/qq/service/QQInterfaces.kt @@ -1,15 +1,129 @@ package qq.service +import android.os.Bundle +import com.tencent.common.app.AppInterface +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.qphone.base.remote.FromServiceMsg +import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeoutOrNull import moe.fuqiuluo.shamrock.utils.PlatformUtils import mqq.app.MobileQQ +import protobuf.auto.toByteArray +import protobuf.oidb.TrpcOidb +import qq.service.internals.MSFHandler +import tencent.im.oidb.oidb_sso +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds abstract class QQInterfaces { - companion object { - val app = if (PlatformUtils.isMqqPackage()) + val app = (if (PlatformUtils.isMqqPackage()) MobileQQ.getMobileQQ().waitAppRuntime() else - MobileQQ.getMobileQQ().waitAppRuntime(null) + MobileQQ.getMobileQQ().waitAppRuntime(null)) as AppInterface + + fun sendToServiceMsg(to: ToServiceMsg) { + app.sendToService(to) + } + + suspend fun sendToServiceMsgAW( + to: ToServiceMsg, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val seq = MSFHandler.nextSeq() + to.addAttribute("shamrock_uid", seq) + val resp: Pair? = withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { continuation -> + GlobalScope.launch { + MSFHandler.registerResp(seq, continuation) + sendToServiceMsg(to) + } + } + } + if (resp == null) { + MSFHandler.unregisterResp(seq) + } + return resp?.second + } + + fun sendExtra(cmd: String, builder: (Bundle) -> Unit) { + val toServiceMsg = createToServiceMsg(cmd) + builder(toServiceMsg.extraData) + app.sendToService(toServiceMsg) + } + + fun createToServiceMsg(cmd: String): ToServiceMsg { + return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd) + } + + fun sendOidb(cmd: String, command: Int, service: Int, data: ByteArray, trpc: Boolean = false) { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(oidb.toByteArray()) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + app.sendToService(to) + } + + @DelicateCoroutinesApi + suspend fun sendBufferAW( + cmd: String, + isProto: Boolean, + data: ByteArray, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val toServiceMsg = createToServiceMsg(cmd) + toServiceMsg.putWupBuffer(data) + toServiceMsg.addAttribute("req_pb_protocol_flag", isProto) + return sendToServiceMsgAW(toServiceMsg, timeout) + } + + @DelicateCoroutinesApi + suspend fun sendOidbAW( + cmd: String, + command: Int, + service: Int, + data: ByteArray, + trpc: Boolean = false, + timeout: Duration = 30.seconds + ): FromServiceMsg? { + val to = createToServiceMsg(cmd) + if (trpc) { + val oidb = TrpcOidb( + cmd = command, + service = service, + buffer = data, + flag = 1 + ) + to.putWupBuffer(oidb.toByteArray()) + } else { + val oidb = oidb_sso.OIDBSSOPkg() + oidb.uint32_command.set(command) + oidb.uint32_service_type.set(service) + oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(data)) + oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext())) + to.putWupBuffer(oidb.toByteArray()) + } + to.addAttribute("req_pb_protocol_flag", true) + return sendToServiceMsgAW(to, timeout) + } } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/contact/ContactHelper.kt b/xposed/src/main/java/qq/service/contact/ContactHelper.kt index 3ddc0a8..30240eb 100644 --- a/xposed/src/main/java/qq/service/contact/ContactHelper.kt +++ b/xposed/src/main/java/qq/service/contact/ContactHelper.kt @@ -3,6 +3,8 @@ package qq.service.contact import com.tencent.common.app.AppInterface import com.tencent.mobileqq.data.Card import com.tencent.mobileqq.profilecard.api.IProfileDataService +import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_SELF_UIN +import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_TARGET_UIN import com.tencent.mobileqq.profilecard.api.IProfileProtocolService import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver import kotlinx.coroutines.suspendCancellableCoroutine @@ -12,9 +14,100 @@ import moe.fuqiuluo.shamrock.internals.NTServiceFetcher import qq.service.QQInterfaces import kotlin.coroutines.resume -object ContactHelper: QQInterfaces() { +internal object ContactHelper: QQInterfaces() { + const val FROM_C2C_AIO = 2 + const val FROM_CONDITION_SEARCH = 9 + const val FROM_CONTACTS_TAB = 5 + const val FROM_FACE_2_FACE_ADD_FRIEND = 11 + const val FROM_MAYKNOW_FRIEND = 3 + const val FROM_QCIRCLE = 4 + const val FROM_QQ_TROOP = 1 + const val FROM_QZONE = 7 + const val FROM_SCAN = 6 + const val FROM_SEARCH = 8 + const val FROM_SETTING_ME = 12 + const val FROM_SHARE_CARD = 10 + + const val PROFILE_CARD_IS_BLACK = 2 + const val PROFILE_CARD_IS_BLACKED = 1 + const val PROFILE_CARD_NOT_BLACK = 3 + + const val SUB_FROM_C2C_AIO = 21 + const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25 + const val SUB_FROM_C2C_LEFT_SLIDE = 23 + const val SUB_FROM_C2C_OTHER = 24 + const val SUB_FROM_C2C_SETTING = 22 + const val SUB_FROM_C2C_TOFU = 26 + const val SUB_FROM_CONDITION_SEARCH_OTHER = 99 + const val SUB_FROM_CONDITION_SEARCH_RESULT = 91 + const val SUB_FROM_CONTACTS_FRIEND_TAB = 51 + const val SUB_FROM_CONTACTS_TAB = 55 + const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111 + const val SUB_FROM_FACE_2_FACE_OTHER = 119 + const val SUB_FROM_FRIEND_APPLY = 56 + const val SUB_FROM_FRIEND_NOTIFY_MORE = 57 + const val SUB_FROM_FRIEND_NOTIFY_TAB = 54 + const val SUB_FROM_GROUPING_TAB = 52 + const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31 + const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39 + const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36 + const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32 + const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35 + const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33 + const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38 + const val SUB_FROM_PHONE_LIST_TAB = 53 + const val SUB_FROM_QCIRCLE_OTHER = 42 + const val SUB_FROM_QCIRCLE_PROFILE = 41 + const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15 + const val SUB_FROM_QQ_TROOP_ADMIN = 16 + const val SUB_FROM_QQ_TROOP_AIO = 11 + const val SUB_FROM_QQ_TROOP_MEMBER = 12 + const val SUB_FROM_QQ_TROOP_OTHER = 14 + const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17 + const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13 + const val SUB_FROM_QRCODE_SCAN_DRAWER = 64 + const val SUB_FROM_QRCODE_SCAN_NEW = 61 + const val SUB_FROM_QRCODE_SCAN_OLD = 62 + const val SUB_FROM_QRCODE_SCAN_OTHER = 69 + const val SUB_FROM_QRCODE_SCAN_PROFILE = 63 + const val SUB_FROM_QZONE_HOME = 71 + const val SUB_FROM_QZONE_OTHER = 79 + const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83 + const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82 + const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84 + const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85 + const val SUB_FROM_SEARCH_OTHER = 89 + const val SUB_FROM_SEARCH_TAB = 81 + const val SUB_FROM_SETTING_ME_AVATAR = 121 + const val SUB_FROM_SETTING_ME_OTHER = 129 + const val SUB_FROM_SHARE_CARD_C2C = 101 + const val SUB_FROM_SHARE_CARD_OTHER = 109 + const val SUB_FROM_SHARE_CARD_TROOP = 102 + const val SUB_FROM_TYPE_DEFAULT = 0 + private val refreshCardLock by lazy { Mutex() } + suspend fun voteUser(target: Long, count: Int): Result { + if(count !in 1 .. 20) { + return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20")) + } + val card = getProfileCard(target).onFailure { + return Result.failure(RuntimeException("unable to fetch contact info")) + }.getOrThrow() + sendExtra("VisitorSvc.ReqFavorite") { + it.putLong(PARAM_SELF_UIN, app.longAccountUin) + it.putLong(PARAM_TARGET_UIN, target) + it.putByteArray("vCookies", card.vCookies) + it.putBoolean("nearby_people", true) + it.putInt("favoriteSource", FROM_CONTACTS_TAB) + it.putInt("iCount", count) + it.putInt("from", FROM_CONTACTS_TAB) + } + return Result.success(Unit) + } + suspend fun getProfileCard(uin: Long): Result { return getProfileCardFromCache(uin).onFailure { return refreshAndGetProfileCard(uin) diff --git a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt new file mode 100644 index 0000000..bf1131b --- /dev/null +++ b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt @@ -0,0 +1,162 @@ +@file:OptIn(ExperimentalStdlibApi::class) + +package qq.service.file + +import com.tencent.mobileqq.pb.ByteStringMicro +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.kritor.file.File +import io.kritor.file.Folder +import io.kritor.file.GetFileSystemInfoResponse +import io.kritor.file.GetFilesRequest +import io.kritor.file.GetFilesResponse +import io.kritor.file.folder +import io.kritor.file.getFileSystemInfoResponse +import io.kritor.file.getFilesRequest +import io.kritor.file.getFilesResponse +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY +import moe.fuqiuluo.shamrock.tools.slice +import moe.fuqiuluo.shamrock.utils.DeflateTools +import qq.service.QQInterfaces +import tencent.im.oidb.cmd0x6d8.oidb_0x6d8 +import tencent.im.oidb.oidb_sso +import kotlin.time.Duration.Companion.seconds + +internal object GroupFileHelper: QQInterfaces() { + suspend fun getGroupFileSystemInfo(groupId: Long): GetFileSystemInfoResponse { + val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also { + it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also { + it.uint64_group_code.set(groupId) + it.uint32_app_id.set(3) + it.uint32_bus_id.set(0) + }) + }.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val fileCnt: Int + val limitCnt: Int + if (fromServiceMsg.wupBuffer != null) { + oidb_0x6d8.RspBody().mergeFrom( + oidb_sso.OIDBSSOPkg() + .mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + .bytes_bodybuffer.get() + .toByteArray() + ).group_file_cnt_rsp.apply { + fileCnt = uint32_all_file_count.get() + limitCnt = uint32_limit_count.get() + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response")) + } + + val fromServiceMsg2 = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also { + it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply { + uint64_group_code.set(groupId) + uint32_app_id.set(3) + }) + }.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + val totalSpace: Long + val usedSpace: Long + if (fromServiceMsg2.isSuccess && fromServiceMsg2.wupBuffer != null) { + oidb_0x6d8.RspBody().mergeFrom( + oidb_sso.OIDBSSOPkg() + .mergeFrom(fromServiceMsg2.wupBuffer.slice(4)) + .bytes_bodybuffer.get() + .toByteArray()).group_space_rsp.apply { + totalSpace = uint64_total_space.get() + usedSpace = uint64_used_space.get() + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2")) + } + + return getFileSystemInfoResponse { + this.fileCount = fileCnt + this.totalCount = limitCnt + this.totalSpace = totalSpace.toInt() + this.usedSpace = usedSpace.toInt() + } + } + + suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFilesResponse { + val fileSystemInfo = getGroupFileSystemInfo(groupId) + val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also { + it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply { + uint64_group_code.set(groupId) + uint32_app_id.set(3) + str_folder_id.set(folderId) + + uint32_file_count.set(fileSystemInfo.fileCount) + uint32_all_file_count.set(0) + uint32_req_from.set(3) + uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP) + + uint32_filter_code.set(0) + uint64_uin.set(0) + + uint32_start_index.set(0) + + bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY)) + + uint32_show_onlinedoc_folder.set(0) + }) + }.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) + if (!fromServiceMsg.isSuccess) { + throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed")) + } + val files = arrayListOf() + val dirs = arrayListOf() + if (fromServiceMsg.wupBuffer != null) { + val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let { + if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it + }) + + oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray()) + .file_list_info_rsp.apply { + rpt_item_list.get().forEach { file -> + if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) { + val fileInfo = file.file_info + files.add(io.kritor.file.file { + this.fileId = fileInfo.str_file_id.get() + this.fileName = fileInfo.str_file_name.get() + this.fileSize = fileInfo.uint64_file_size.get() + this.busId = fileInfo.uint32_bus_id.get() + this.uploadTime = fileInfo.uint32_upload_time.get() + this.deadTime = fileInfo.uint32_dead_time.get() + this.modifyTime = fileInfo.uint32_modify_time.get() + this.downloadTimes = fileInfo.uint32_download_times.get() + this.uploader = fileInfo.uint64_uploader_uin.get() + this.uploaderName = fileInfo.str_uploader_name.get() + this.sha = fileInfo.bytes_sha.get().toByteArray().toHexString() + this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString() + this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString() + }) + } + else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) { + val folderInfo = file.folder_info + dirs.add(folder { + this.folderId = folderInfo.str_folder_id.get() + this.folderName = folderInfo.str_folder_name.get() + this.totalFileCount = folderInfo.uint32_total_file_count.get() + this.createTime = folderInfo.uint32_create_time.get() + this.creator = folderInfo.uint64_create_uin.get() + this.creatorName = folderInfo.str_creator_name.get() + }) + } else { + LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN) + } + } + } + } else { + throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response")) + } + + return getFilesResponse { + this.files.addAll(files) + this.folders.addAll(folders) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/friend/FriendHelper.kt b/xposed/src/main/java/qq/service/friend/FriendHelper.kt new file mode 100644 index 0000000..d93921e --- /dev/null +++ b/xposed/src/main/java/qq/service/friend/FriendHelper.kt @@ -0,0 +1,40 @@ +package qq.service.friend + +import com.tencent.mobileqq.data.Friends +import com.tencent.mobileqq.friend.api.IFriendDataService +import com.tencent.mobileqq.friend.api.IFriendHandlerService +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import qq.service.QQInterfaces +import kotlin.coroutines.resume + +internal object FriendHelper: QQInterfaces() { + suspend fun getFriendList(refresh: Boolean): Result> { + val service = app.getRuntimeService(IFriendDataService::class.java, "all") + if(refresh || !service.isInitFinished) { + if(!requestFriendList(service)) { + return Result.failure(Exception("获取好友列表失败")) + } + } + return Result.success(service.allFriends) + } + + private suspend fun requestFriendList(dataService: IFriendDataService): Boolean { + val service = app.getRuntimeService(IFriendHandlerService::class.java, "all") + service.requestFriendList(true, 0) + return suspendCancellableCoroutine { continuation -> + val waiter = GlobalScope.launch { + while (!dataService.isInitFinished) { + delay(200) + } + continuation.resume(true) + } + continuation.invokeOnCancellation { + waiter.cancel() + continuation.resume(false) + } + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt new file mode 100644 index 0000000..ca57424 --- /dev/null +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -0,0 +1,617 @@ +package qq.service.group + +import KQQ.RespBatchProcess +import com.qq.jce.wup.UniPacket +import com.tencent.mobileqq.app.BusinessHandlerFactory +import com.tencent.mobileqq.data.troop.TroopInfo +import com.tencent.mobileqq.data.troop.TroopMemberInfo +import com.tencent.mobileqq.pb.ByteStringMicro +import com.tencent.mobileqq.troop.api.ITroopInfoService +import com.tencent.mobileqq.troop.api.ITroopMemberInfoService +import com.tencent.qqnt.kernel.nativeinterface.MemberInfo +import friendlist.stUinInfo +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withTimeoutOrNull +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.internals.NTServiceFetcher +import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty +import moe.fuqiuluo.shamrock.tools.putBuf32Long +import moe.fuqiuluo.shamrock.tools.slice +import protobuf.auto.toByteArray +import protobuf.oidb.cmd0xf16.Oidb0xf16 +import protobuf.oidb.cmd0xf16.SetGroupRemarkReq +import qq.service.QQInterfaces +import tencent.im.group.group_member_info +import tencent.im.oidb.cmd0x88d.oidb_0x88d +import tencent.im.oidb.cmd0x899.oidb_0x899 +import tencent.im.oidb.cmd0x89a.oidb_0x89a +import tencent.im.oidb.cmd0x8a0.oidb_0x8a0 +import tencent.im.oidb.cmd0x8a7.cmd0x8a7 +import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc +import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3 +import tencent.im.oidb.oidb_sso +import tencent.im.troop.honor.troop_honor +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.nio.ByteBuffer +import kotlin.coroutines.resume + +internal object GroupHelper: QQInterfaces() { + private val RefreshTroopMemberInfoLock by lazy { Mutex() } + private val RefreshTroopMemberListLock by lazy { Mutex() } + + private lateinit var METHOD_REQ_MEMBER_INFO: Method + private lateinit var METHOD_REQ_MEMBER_INFO_V2: Method + private lateinit var METHOD_REQ_TROOP_LIST: Method + private lateinit var METHOD_REQ_TROOP_MEM_LIST: Method + private lateinit var METHOD_REQ_MODIFY_GROUP_NAME: Method + + fun getGroupInfo(groupId: String): TroopInfo { + val service = app + .getRuntimeService(ITroopInfoService::class.java, "all") + + return service.getTroopInfo(groupId) + } + + fun isAdmin(groupId: String): Boolean { + val groupInfo = getGroupInfo(groupId) + + return groupInfo.isAdmin || groupInfo.troopowneruin == app.account + } + + fun isOwner(groupId: String): Boolean { + val groupInfo = getGroupInfo(groupId) + return groupInfo.troopowneruin == app.account + } + + fun getAdminList( + groupId: Long, + withOwner: Boolean = false + ): List { + val groupInfo = getGroupInfo(groupId.toString()) + return (groupInfo.Administrator ?: "") + .split("|", ",") + .also { + if (withOwner && it is ArrayList) { + it.add(0, groupInfo.troopowneruin) + } + }.mapNotNull { it.ifNullOrEmpty { null }?.toLong() } + } + + suspend fun getGroupList(refresh: Boolean): Result> { + val service = app.getRuntimeService(ITroopInfoService::class.java, "all") + + var troopList = service.allTroopList + if(refresh || !service.isTroopCacheInited || troopList == null) { + if(!requestGroupInfo(service)) { + return Result.failure(Exception("获取群列表失败")) + } else { + troopList = service.allTroopList + } + } + return Result.success(troopList) + } + + private suspend fun requestGroupInfo( + service: ITroopInfoService + ): Boolean { + refreshTroopList() + + return suspendCancellableCoroutine { continuation -> + val waiter = GlobalScope.launch { + do { + delay(1000) + } while ( + !service.isTroopCacheInited + ) + continuation.resume(true) + } + continuation.invokeOnCancellation { + waiter.cancel() + continuation.resume(false) + } + } + } + + fun banMember(groupId: Long, memberUin: Long, time: Int) { + val buffer = ByteBuffer.allocate(1 * 8 + 7) + buffer.putBuf32Long(groupId) + buffer.put(32.toByte()) + buffer.putShort(1) + buffer.putBuf32Long(memberUin) + buffer.putInt(time) + val array = buffer.array() + sendOidb("OidbSvc.0x570_8", 1392, 8, array) + } + + fun pokeMember(groupId: Long, memberUin: Long) { + val req = oidb_cmd0xed3.ReqBody().apply { + uint64_group_code.set(groupId) + uint64_to_uin.set(memberUin) + uint32_msg_seq.set(0) + } + sendOidb("OidbSvc.0xed3", 3795, 1, req.toByteArray()) + } + + fun kickMember(groupId: Long, rejectAddRequest: Boolean, kickMsg: String, vararg memberUin: Long) { + val reqBody = oidb_0x8a0.ReqBody() + reqBody.opt_uint64_group_code.set(groupId) + memberUin.forEach { + val memberInfo = oidb_0x8a0.KickMemberInfo() + memberInfo.opt_uint32_operate.set(5) + memberInfo.opt_uint64_member_uin.set(it) + memberInfo.opt_uint32_flag.set(if (rejectAddRequest) 1 else 0) + reqBody.rpt_msg_kick_list.add(memberInfo) + } + if (kickMsg.isNotEmpty()) { + reqBody.bytes_kick_msg.set(ByteStringMicro.copyFrom(kickMsg.toByteArray())) + } + sendOidb("OidbSvc.0x8a0_0", 2208, 0, reqBody.toByteArray()) + } + + fun resignTroop(groupId: String) { + sendExtra("ProfileService.GroupMngReq") { + it.putInt("groupreqtype", 2) + it.putString("troop_uin", groupId) + it.putString("uin", app.currentUin) + } + } + + fun modifyGroupMemberCard(groupId: Long, userId: Long, name: String): Boolean { + val createToServiceMsg = createToServiceMsg("friendlist.ModifyGroupCardReq") + createToServiceMsg.extraData.putLong("dwZero", 0L) + createToServiceMsg.extraData.putLong("dwGroupCode", groupId) + val info = stUinInfo() + info.cGender = -1 + info.dwuin = userId + info.sEmail = "" + info.sName = name + info.sPhone = "" + info.sRemark = "" + info.dwFlag = 1 + createToServiceMsg.extraData.putSerializable("vecUinInfo", arrayListOf(info)) + createToServiceMsg.extraData.putLong("dwNewSeq", 0L) + sendToServiceMsg(createToServiceMsg) + return true + } + + fun modifyTroopName(groupId: String, name: String) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MODIFY_HANDLER) + + if (!::METHOD_REQ_MODIFY_GROUP_NAME.isInitialized) { + METHOD_REQ_MODIFY_GROUP_NAME = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 3 + && it.parameterTypes[0] == String::class.java + && it.parameterTypes[1] == String::class.java + && it.parameterTypes[2] == Boolean::class.java + && !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MODIFY_GROUP_NAME.invoke(businessHandler, groupId, name, false) + } + + fun modifyGroupRemark(groupId: Long, remark: String): Boolean { + sendOidb("OidbSvc.0xf16_1", 3862, 1, Oidb0xf16( + setGroupRemarkReq = SetGroupRemarkReq( + groupCode = groupId.toULong(), + groupUin = groupCode2GroupUin(groupId).toULong(), + groupRemark = remark + ) + ).toByteArray()) + return true + } + + fun setGroupAdmin(groupId: Long, userId: Long, enable: Boolean) { + val buffer = ByteBuffer.allocate(9) + buffer.putBuf32Long(groupId) + buffer.putBuf32Long(userId) + buffer.put(if (enable) 1 else 0) + val array = buffer.array() + sendOidb("OidbSvc.0x55c_1", 1372, 1, array) + } + + suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) { + val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow() + val req = Oidb_0x8fc.ReqBody() + req.uint64_group_code.set(groupId) + val memberInfo = Oidb_0x8fc.MemberInfo() + memberInfo.uint64_uin.set(userId) + memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty { + localMemberInfo.troopremark.ifNullOrEmpty { "" } + })) + memberInfo.bytes_special_title.set(ByteStringMicro.copyFromUtf8(title)) + memberInfo.uint32_special_title_expire_time.set(-1) + req.rpt_mem_level_info.add(memberInfo) + sendOidb("OidbSvc.0x8fc_2", 2300, 2, req.toByteArray()) + } + + fun setGroupWholeBan(groupId: Long, enable: Boolean) { + val reqBody = oidb_0x89a.ReqBody() + reqBody.uint64_group_code.set(groupId) + reqBody.st_group_info.set(oidb_0x89a.groupinfo().apply { + uint32_shutup_time.set(if (enable) 268435455 else 0) + }) + sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray()) + } + + suspend fun getGroupMemberList(groupId: String, refresh: Boolean): Result> { + val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") + var memberList = service.getAllTroopMembers(groupId) + if (refresh || memberList == null) { + memberList = requestTroopMemberInfo(service, groupId).onFailure { + return Result.failure(Exception("获取群成员列表失败")) + }.getOrThrow() + } + + getGroupInfo(groupId, true).onSuccess { + if(it.wMemberNum > memberList.size) { + return getGroupMemberList(groupId, true) + } + } + + return Result.success(memberList) + } + + suspend fun getProhibitedMemberList(groupId: Long): Result> { + val fromServiceMsg = sendOidbAW("OidbSvc.0x899_0", 2201, 0, oidb_0x899.ReqBody().apply { + uint64_group_code.set(groupId) + uint64_start_uin.set(0) + uint32_identify_flag.set(6) + memberlist_opt.set(oidb_0x899.memberlist().apply { + uint64_member_uin.set(0) + uint32_shutup_timestap.set(0) + }) + }.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout")) + if (!fromServiceMsg.isSuccess) { + return Result.failure(RuntimeException("[oidb] failed")) + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + if(body.uint32_result.get() != 0) { + return Result.failure(RuntimeException(body.str_error_msg.get())) + } + val resp = oidb_0x899.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return Result.success(resp.rpt_memberlist.get().map { + ProhibitedMemberInfo(it.uint64_member_uin.get(), it.uint32_shutup_timestap.get()) + }) + } + + suspend fun getGroupRemainAtAllRemain (groupId: Long): Result { + val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x8a7_0", 2215, 0, cmd0x8a7.ReqBody().apply { + uint32_sub_cmd.set(1) + uint32_limit_interval_type_for_uin.set(2) + uint32_limit_interval_type_for_group.set(1) + uint64_uin.set(app.longAccountUin) + uint64_group_code.set(groupId) + }.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout")) + if (!fromServiceMsg.isSuccess) { + return Result.failure(RuntimeException("[oidb] failed")) + } + val body = oidb_sso.OIDBSSOPkg() + body.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + if(body.uint32_result.get() != 0) { + return Result.failure(RuntimeException(body.str_error_msg.get())) + } + + val resp = cmd0x8a7.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray()) + return Result.success(resp) + } + + suspend fun getNotJoinedGroupInfo(groupId: Long): Result { + return withTimeoutOrNull(5000) timeout@{ + val toServiceMsg = createToServiceMsg("ProfileService.ReqBatchProcess") + toServiceMsg.extraData.putLong("troop_code", groupId) + toServiceMsg.extraData.putBoolean("is_admin", false) + toServiceMsg.extraData.putInt("from", 0) + val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时")) + if (!fromServiceMsg.isSuccess) { + return@timeout Result.failure(Exception("获取群信息失败")) + } + val uniPacket = UniPacket(true) + uniPacket.encodeName = "utf-8" + uniPacket.decode(fromServiceMsg.wupBuffer) + val respBatchProcess = uniPacket.getByClass("RespBatchProcess", RespBatchProcess()) + val batchRespInfo = oidb_0x88d.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg() + .mergeFrom(respBatchProcess.batch_response_list.first().buffer) + .bytes_bodybuffer.get().toByteArray()).stzrspgroupinfo.get().firstOrNull() + ?: return@timeout Result.failure(Exception("获取群信息失败")) + val info = batchRespInfo.stgroupinfo + Result.success(NotJoinedGroupInfo( + groupId = batchRespInfo.uint64_group_code.get(), + maxMember = info.uint32_group_member_max_num.get(), + memberCount = info.uint32_group_member_num.get(), + groupName = info.string_group_name.get().toStringUtf8(), + groupDesc = info.string_group_finger_memo.get().toStringUtf8(), + owner = info.uint64_group_owner.get(), + createTime = info.uint32_group_create_time.get().toLong(), + groupFlag = info.uint32_group_flag.get(), + groupFlagExt = info.uint32_group_flag_ext.get() + )) + } ?: Result.failure(Exception("获取群信息超时")) + } + + suspend fun getGroupInfo(groupId: String, refresh: Boolean): Result { + val service = app + .getRuntimeService(ITroopInfoService::class.java, "all") + + val groupInfo = getGroupInfo(groupId) + + return if(refresh || !service.isTroopCacheInited || groupInfo.troopuin.isNullOrBlank()) { + requestGroupInfo(service, groupId) + } else { + Result.success(groupInfo) + } + } + + private suspend fun requestGroupInfo(dataService: ITroopInfoService, uin: String): Result { + val info = withTimeoutOrNull(1000) { + var troopInfo: TroopInfo? + do { + troopInfo = dataService.getTroopInfo(uin) + delay(100) + } while (troopInfo == null || troopInfo.troopuin.isNullOrBlank()) + return@withTimeoutOrNull troopInfo + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群列表失败")) + } + } + + suspend fun getTroopMemberInfoByUin( + groupId: Long, + uin: Long, + refresh: Boolean = false + ): Result { + val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all") + var info = service.getTroopMember(groupId.toString(), uin.toString()) + if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) { + info = requestTroopMemberInfo(service, groupId, uin).getOrNull() + } + if (info == null) { + info = getTroopMemberInfoByUinViaNt(groupId, uin).getOrNull()?.let { + TroopMemberInfo().apply { + troopnick = it.cardName + friendnick = it.nick + } + } + } + try { + if (info != null && (info.alias == null || info.alias.isBlank())) { + val req = group_member_info.ReqBody() + req.uint64_group_code.set(groupId) + req.uint64_uin.set(uin) + req.bool_new_client.set(true) + req.uint32_client_type.set(1) + req.uint32_rich_card_name_ver.set(1) + val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray()) + if (fromServiceMsg != null && fromServiceMsg.isSuccess) { + val rsp = group_member_info.RspBody() + rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) + if (rsp.msg_meminfo.str_location.has()) { + info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8() + } + if (rsp.msg_meminfo.uint32_age.has()) { + info.age = rsp.msg_meminfo.uint32_age.get().toByte() + } + if (rsp.msg_meminfo.bytes_group_honor.has()) { + val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray() + val honor = troop_honor.GroupUserCardHonor() + honor.mergeFrom(honorBytes) + info.level = honor.level.get() + // 10315: medal_id not real group level + } + } + } + } catch (err: Throwable) { + LogCenter.log("getTroopMemberInfoByUin: " + err.stackTraceToString(), Level.WARN) + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + suspend fun getTroopMemberInfoByUinViaNt( + groupId: Long, + qq: Long, + timeout: Long = 5000L + ): Result { + return runCatching { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val groupService = sessionService.groupService + val info = withTimeoutOrNull(timeout) { + suspendCancellableCoroutine { + groupService.getTransferableMemberInfo(groupId) { code, _, data -> + if (code != 0) { + it.resume(null) + return@getTransferableMemberInfo + } + data.forEach { (_, info) -> + if (info.uin == qq) { + it.resume(info) + return@forEach + } + } + it.resume(null) + } + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + } + + private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result { + val info = RefreshTroopMemberInfoLock.withLock { + val groupIdStr = groupId.toString() + val memberUinStr = memberUin.toString() + + service.deleteTroopMember(groupIdStr, memberUinStr) + + requestMemberInfoV2(groupId, memberUin) + requestMemberInfo(groupId, memberUin) + + withTimeoutOrNull(timeout) { + while (!service.isMemberInCache(groupIdStr, memberUinStr)) { + delay(200) + } + return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr) + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + private fun requestMemberInfo(groupId: Long, memberUin: Long) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) + + if (!::METHOD_REQ_MEMBER_INFO.isInitialized) { + METHOD_REQ_MEMBER_INFO = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 2 && + it.parameterTypes[0] == Long::class.java && + it.parameterTypes[1] == Long::class.java && + !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin) + } + + private fun requestMemberInfoV2(groupId: Long, memberUin: Long) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER) + + if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) { + METHOD_REQ_MEMBER_INFO_V2 = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 3 && + it.parameterTypes[0] == String::class.java && + it.parameterTypes[1] == String::class.java && + !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString())) + } + + private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result> { + val info = RefreshTroopMemberListLock.withLock { + service.deleteTroopMembers(groupId) + refreshTroopMemberList(groupId) + + withTimeoutOrNull(10000) { + var memberList: List? + do { + delay(100) + memberList = service.getAllTroopMembers(groupId) + } while (memberList.isNullOrEmpty()) + return@withTimeoutOrNull memberList + } + } + return if (info != null) { + Result.success(info) + } else { + Result.failure(Exception("获取群成员信息失败")) + } + } + + private fun refreshTroopMemberList(groupId: String) { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_LIST_HANDLER) + + // void C(boolean forceRefresh, String groupId, String troopcode, int reqType); // RequestedTroopList/refreshMemberListFromServer + if (!::METHOD_REQ_TROOP_MEM_LIST.isInitialized) { + METHOD_REQ_TROOP_MEM_LIST = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 4 + && it.parameterTypes[0] == Boolean::class.java + && it.parameterTypes[1] == String::class.java + && it.parameterTypes[2] == String::class.java + && it.parameterTypes[3] == Int::class.java + && !Modifier.isPrivate(it.modifiers) + } + } + + METHOD_REQ_TROOP_MEM_LIST.invoke(businessHandler, true, groupId, groupUin2GroupCode(groupId.toLong()).toString(), 5) + } + + private fun refreshTroopList() { + val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_LIST_HANDLER) + + if (!::METHOD_REQ_TROOP_LIST.isInitialized) { + METHOD_REQ_TROOP_LIST = businessHandler.javaClass.declaredMethods.first { + it.parameterCount == 0 && !Modifier.isPrivate(it.modifiers) && it.returnType == Void.TYPE + } + } + + METHOD_REQ_TROOP_LIST.invoke(businessHandler) + } + + fun groupUin2GroupCode(groupuin: Long): Long { + var calc = groupuin / 1000000L + while (true) { + calc -= if (calc >= 0 + 202 && calc + 202 <= 10) { + (202 - 0).toLong() + } else if (calc >= 11 + 480 && calc <= 19 + 480) { + (480 - 11).toLong() + } else if (calc >= 20 + 2100 && calc <= 66 + 2100) { + (2100 - 20).toLong() + } else if (calc >= 67 + 2010 && calc <= 156 + 2010) { + (2010 - 67).toLong() + } else if (calc >= 157 + 2147 && calc <= 209 + 2147) { + (2147 - 157).toLong() + } else if (calc >= 210 + 4100 && calc <= 309 + 4100) { + (4100 - 210).toLong() + } else if (calc >= 310 + 3800 && calc <= 499 + 3800) { + (3800 - 310).toLong() + } else { + break + } + } + return calc * 1000000L + groupuin % 1000000L + } + + fun groupCode2GroupUin(groupcode: Long): Long { + var calc = groupcode / 1000000L + loop@ while (true) calc += when (calc) { + in 0..10 -> { + (202 - 0).toLong() + } + in 11..19 -> { + (480 - 11).toLong() + } + in 20..66 -> { + (2100 - 20).toLong() + } + in 67..156 -> { + (2010 - 67).toLong() + } + in 157..209 -> { + (2147 - 157).toLong() + } + in 210..309 -> { + (4100 - 210).toLong() + } + in 310..499 -> { + (3800 - 310).toLong() + } + else -> { + break@loop + } + } + return calc * 1000000L + groupcode % 1000000L + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt b/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt new file mode 100644 index 0000000..9959e8f --- /dev/null +++ b/xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt @@ -0,0 +1,17 @@ +package qq.service.group + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class NotJoinedGroupInfo( + @SerialName("group_id") val groupId: Long, + @SerialName("max_member_cnt") val maxMember: Int, + @SerialName("member_count") val memberCount: Int, + @SerialName("group_name") val groupName: String, + @SerialName("group_desc") val groupDesc: String, + @SerialName("owner") val owner: Long, + @SerialName("create_time") val createTime: Long, + @SerialName("group_flag") val groupFlag: Int, + @SerialName("group_flag_ext") val groupFlagExt: Int, +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt b/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt new file mode 100644 index 0000000..6611dd7 --- /dev/null +++ b/xposed/src/main/java/qq/service/group/ProhibitedMemberInfo.kt @@ -0,0 +1,10 @@ +package qq.service.group + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal data class ProhibitedMemberInfo( + @SerialName("user_id") val memberUin: Long, + @SerialName("time") val shutuptimestap: Int +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/internals/MSFHandler.kt b/xposed/src/main/java/qq/service/internals/MSFHandler.kt index 7883266..2317332 100644 --- a/xposed/src/main/java/qq/service/internals/MSFHandler.kt +++ b/xposed/src/main/java/qq/service/internals/MSFHandler.kt @@ -2,13 +2,16 @@ package qq.service.internals import com.tencent.qphone.base.remote.FromServiceMsg import com.tencent.qphone.base.remote.ToServiceMsg +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import kotlin.coroutines.resume typealias MsfPush = (FromServiceMsg) -> Unit -typealias MsfResp = (ToServiceMsg, FromServiceMsg) -> Unit +typealias MsfResp = CancellableContinuation> internal object MSFHandler { private val mPushHandlers = hashMapOf() @@ -16,6 +19,13 @@ internal object MSFHandler { private val mPushLock = Mutex() private val mRespLock = Mutex() + private val seq = atomic(0) + + fun nextSeq(): Int { + seq.compareAndSet(0xFFFFFFF, 0) + return seq.incrementAndGet() + } + suspend fun registerPush(cmd: String, push: MsfPush) { mPushLock.withLock { mPushHandlers[cmd] = push @@ -51,7 +61,7 @@ internal object MSFHandler { val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int? ?: return@runCatching val resp = mRespHandler[cmd] - resp?.invoke(toServiceMsg, fromServiceMsg) + resp?.resume(toServiceMsg to fromServiceMsg) }.onFailure { LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR) }