Shamrock: グループファイルサービスの実装

Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
白池 2024-03-12 18:46:05 +08:00
parent cb4268edef
commit ca47f9dbdf
16 changed files with 1693 additions and 14 deletions

2
kritor

@ -1 +1 @@
Subproject commit 201e91e73225ce3f1ec098c06a5cf6a717d913e5
Subproject commit f007631c7e17fe6220055a404fdaaa6e7a24a7ef

View File

@ -21,6 +21,9 @@ class KritorServer(
.addService(Authentication)
.addService(ContactService)
.addService(KritorService)
.addService(FriendService)
.addService(GroupService)
.addService(GroupFileService)
.build()!!
fun start(block: Boolean = false) {

View File

@ -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 ?: ""

View 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()
})
}
}
}
}

View 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)
}
}

View 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
})
}
}
}
}
}
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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<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)
}
}
}

View File

@ -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<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> {
return getProfileCardFromCache(uin).onFailure {
return refreshAndGetProfileCard(uin)

View 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)
}
}
}

View 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)
}
}
}
}

View 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
}
}

View 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,
)

View File

@ -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
)

View File

@ -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<Pair<ToServiceMsg, FromServiceMsg>>
internal object MSFHandler {
private val mPushHandlers = hashMapOf<String, MsfPush>()
@ -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)
}