mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
Shamrock
: グループファイルサービスの実装
Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
parent
cb4268edef
commit
ca47f9dbdf
2
kritor
2
kritor
@ -1 +1 @@
|
|||||||
Subproject commit 201e91e73225ce3f1ec098c06a5cf6a717d913e5
|
Subproject commit f007631c7e17fe6220055a404fdaaa6e7a24a7ef
|
@ -21,6 +21,9 @@ class KritorServer(
|
|||||||
.addService(Authentication)
|
.addService(Authentication)
|
||||||
.addService(ContactService)
|
.addService(ContactService)
|
||||||
.addService(KritorService)
|
.addService(KritorService)
|
||||||
|
.addService(FriendService)
|
||||||
|
.addService(GroupService)
|
||||||
|
.addService(GroupFileService)
|
||||||
.build()!!
|
.build()!!
|
||||||
|
|
||||||
fun start(block: Boolean = false) {
|
fun start(block: Boolean = false) {
|
||||||
|
@ -21,8 +21,11 @@ import io.kritor.contact.SetProfileCardResponse
|
|||||||
import io.kritor.contact.StrangerExt
|
import io.kritor.contact.StrangerExt
|
||||||
import io.kritor.contact.StrangerInfo
|
import io.kritor.contact.StrangerInfo
|
||||||
import io.kritor.contact.StrangerInfoRequest
|
import io.kritor.contact.StrangerInfoRequest
|
||||||
|
import io.kritor.contact.VoteUserRequest
|
||||||
|
import io.kritor.contact.VoteUserResponse
|
||||||
import io.kritor.contact.profileCard
|
import io.kritor.contact.profileCard
|
||||||
import io.kritor.contact.strangerInfo
|
import io.kritor.contact.strangerInfo
|
||||||
|
import io.kritor.contact.voteUserResponse
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import qq.service.QQInterfaces
|
import qq.service.QQInterfaces
|
||||||
@ -31,10 +34,32 @@ import kotlin.coroutines.resume
|
|||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
|
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")
|
@Grpc("ContactService", "GetProfileCard")
|
||||||
override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard {
|
override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard {
|
||||||
val uin = if (request.hasUin()) request.uin
|
val uin = when (request.accountCase!!) {
|
||||||
else ContactHelper.getUinByUidAsync(request.uid).toLong()
|
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)
|
val contact = ContactHelper.getProfileCard(uin)
|
||||||
|
|
||||||
contact.onFailure {
|
contact.onFailure {
|
||||||
@ -46,7 +71,7 @@ object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
|
|||||||
contact.onSuccess {
|
contact.onSuccess {
|
||||||
return profileCard {
|
return profileCard {
|
||||||
this.uin = it.uin.toLong()
|
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())
|
else ContactHelper.getUidByUinAsync(it.uin.toLong())
|
||||||
this.name = it.strNick ?: ""
|
this.name = it.strNick ?: ""
|
||||||
this.remark = it.strReMark ?: ""
|
this.remark = it.strReMark ?: ""
|
||||||
|
41
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
41
xposed/src/main/java/kritor/service/FriendService.kt
Normal file
@ -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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
138
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
138
xposed/src/main/java/kritor/service/GroupFileService.kt
Normal file
@ -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<Oidb0x6d7RespBody>()
|
||||||
|
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<Oidb0x6d7RespBody>()
|
||||||
|
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<Oidb0x6d7RespBody>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
406
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
406
xposed/src/main/java/kritor/service/GroupService.kt
Normal file
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -104,8 +104,11 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
|
|||||||
|
|
||||||
@Grpc("KritorService", "SwitchAccount")
|
@Grpc("KritorService", "SwitchAccount")
|
||||||
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
|
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
|
||||||
val uin = if (request.hasAccountUin()) request.accountUin.toString()
|
val uin = when(request.accountCase!!) {
|
||||||
else ContactHelper.getUinByUidAsync(request.accountUid)
|
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 }
|
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
|
||||||
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
|
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
|
||||||
runCatching {
|
runCatching {
|
||||||
|
@ -47,8 +47,8 @@ fun ByteArray.slice(off: Int, length: Int = size - off): ByteArray {
|
|||||||
.let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s }
|
.let { s -> if (uppercase) s.lowercase(Locale.getDefault()) else s }
|
||||||
} ?: "null"
|
} ?: "null"
|
||||||
|
|
||||||
fun String?.ifNullOrEmpty(defaultValue: String?): String? {
|
fun String?.ifNullOrEmpty(defaultValue: () -> String?): String? {
|
||||||
return if (this.isNullOrEmpty()) defaultValue else this
|
return if (this.isNullOrEmpty()) defaultValue() else this
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray {
|
@JvmOverloads fun String.hex2ByteArray(replace: Boolean = false): ByteArray {
|
||||||
|
@ -1,15 +1,129 @@
|
|||||||
package qq.service
|
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 moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
import mqq.app.MobileQQ
|
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 {
|
abstract class QQInterfaces {
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val app = if (PlatformUtils.isMqqPackage())
|
val app = (if (PlatformUtils.isMqqPackage())
|
||||||
MobileQQ.getMobileQQ().waitAppRuntime()
|
MobileQQ.getMobileQQ().waitAppRuntime()
|
||||||
else
|
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<ToServiceMsg, FromServiceMsg>? = 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,6 +3,8 @@ package qq.service.contact
|
|||||||
import com.tencent.common.app.AppInterface
|
import com.tencent.common.app.AppInterface
|
||||||
import com.tencent.mobileqq.data.Card
|
import com.tencent.mobileqq.data.Card
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileDataService
|
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.api.IProfileProtocolService
|
||||||
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
@ -12,9 +14,100 @@ import moe.fuqiuluo.shamrock.internals.NTServiceFetcher
|
|||||||
import qq.service.QQInterfaces
|
import qq.service.QQInterfaces
|
||||||
import kotlin.coroutines.resume
|
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() }
|
private val refreshCardLock by lazy { Mutex() }
|
||||||
|
|
||||||
|
suspend fun voteUser(target: Long, count: Int): Result<Unit> {
|
||||||
|
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<Card> {
|
suspend fun getProfileCard(uin: Long): Result<Card> {
|
||||||
return getProfileCardFromCache(uin).onFailure {
|
return getProfileCardFromCache(uin).onFailure {
|
||||||
return refreshAndGetProfileCard(uin)
|
return refreshAndGetProfileCard(uin)
|
||||||
|
162
xposed/src/main/java/qq/service/file/GroupFileHelper.kt
Normal file
162
xposed/src/main/java/qq/service/file/GroupFileHelper.kt
Normal file
@ -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<File>()
|
||||||
|
val dirs = arrayListOf<Folder>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
xposed/src/main/java/qq/service/friend/FriendHelper.kt
Normal file
40
xposed/src/main/java/qq/service/friend/FriendHelper.kt
Normal file
@ -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<List<Friends>> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
617
xposed/src/main/java/qq/service/group/GroupHelper.kt
Normal file
617
xposed/src/main/java/qq/service/group/GroupHelper.kt
Normal file
@ -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<Long> {
|
||||||
|
val groupInfo = getGroupInfo(groupId.toString())
|
||||||
|
return (groupInfo.Administrator ?: "")
|
||||||
|
.split("|", ",")
|
||||||
|
.also {
|
||||||
|
if (withOwner && it is ArrayList<String>) {
|
||||||
|
it.add(0, groupInfo.troopowneruin)
|
||||||
|
}
|
||||||
|
}.mapNotNull { it.ifNullOrEmpty { null }?.toLong() }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getGroupList(refresh: Boolean): Result<List<TroopInfo>> {
|
||||||
|
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<List<TroopMemberInfo>> {
|
||||||
|
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<List<ProhibitedMemberInfo>> {
|
||||||
|
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<cmd0x8a7.RspBody> {
|
||||||
|
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<NotJoinedGroupInfo> {
|
||||||
|
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<TroopInfo> {
|
||||||
|
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<TroopInfo> {
|
||||||
|
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<TroopMemberInfo> {
|
||||||
|
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<MemberInfo> {
|
||||||
|
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<TroopMemberInfo> {
|
||||||
|
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<List<TroopMemberInfo>> {
|
||||||
|
val info = RefreshTroopMemberListLock.withLock {
|
||||||
|
service.deleteTroopMembers(groupId)
|
||||||
|
refreshTroopMemberList(groupId)
|
||||||
|
|
||||||
|
withTimeoutOrNull(10000) {
|
||||||
|
var memberList: List<TroopMemberInfo>?
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
17
xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt
Normal file
17
xposed/src/main/java/qq/service/group/NotJoinedGroupInfo.kt
Normal file
@ -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,
|
||||||
|
)
|
@ -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
|
||||||
|
)
|
@ -2,13 +2,16 @@ package qq.service.internals
|
|||||||
|
|
||||||
import com.tencent.qphone.base.remote.FromServiceMsg
|
import com.tencent.qphone.base.remote.FromServiceMsg
|
||||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||||
|
import kotlinx.atomicfu.atomic
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
typealias MsfPush = (FromServiceMsg) -> Unit
|
typealias MsfPush = (FromServiceMsg) -> Unit
|
||||||
typealias MsfResp = (ToServiceMsg, FromServiceMsg) -> Unit
|
typealias MsfResp = CancellableContinuation<Pair<ToServiceMsg, FromServiceMsg>>
|
||||||
|
|
||||||
internal object MSFHandler {
|
internal object MSFHandler {
|
||||||
private val mPushHandlers = hashMapOf<String, MsfPush>()
|
private val mPushHandlers = hashMapOf<String, MsfPush>()
|
||||||
@ -16,6 +19,13 @@ internal object MSFHandler {
|
|||||||
private val mPushLock = Mutex()
|
private val mPushLock = Mutex()
|
||||||
private val mRespLock = 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) {
|
suspend fun registerPush(cmd: String, push: MsfPush) {
|
||||||
mPushLock.withLock {
|
mPushLock.withLock {
|
||||||
mPushHandlers[cmd] = push
|
mPushHandlers[cmd] = push
|
||||||
@ -51,7 +61,7 @@ internal object MSFHandler {
|
|||||||
val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int?
|
val cmd = toServiceMsg.getAttribute("shamrock_uid") as? Int?
|
||||||
?: return@runCatching
|
?: return@runCatching
|
||||||
val resp = mRespHandler[cmd]
|
val resp = mRespHandler[cmd]
|
||||||
resp?.invoke(toServiceMsg, fromServiceMsg)
|
resp?.resume(toServiceMsg to fromServiceMsg)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR)
|
LogCenter.log("MSF.onResp failed: ${it.message}", Level.ERROR)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user