2 Commits

Author SHA1 Message Date
cb4268edef Shamrock: 实现Kritor核心服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:48:11 +08:00
c16f9d543c Shamrock: 实现联系人服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-11 12:30:42 +08:00
7 changed files with 254 additions and 7 deletions

2
kritor

Submodule kritor updated: 505d80b2ee...201e91e732

View File

@ -42,7 +42,7 @@ object AuthInterceptor: ServerInterceptor {
}
}
private fun getAllTicket(): List<String> {
fun getAllTicket(): List<String> {
val result = arrayListOf<String>()
val activeTicketName = ActiveTicket.name()
var index = 0

View File

@ -20,6 +20,7 @@ class KritorServer(
.intercept(AuthInterceptor)
.addService(Authentication)
.addService(ContactService)
.addService(KritorService)
.build()!!
fun start(block: Boolean = false) {

View File

@ -1,10 +1,16 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.AuthCode
import io.kritor.AuthReq
import io.kritor.AuthRsp
import io.kritor.AuthenticationGrpcKt
import io.kritor.GetAuthStateReq
import io.kritor.GetAuthStateRsp
import io.kritor.authRsp
import io.kritor.getAuthStateRsp
import kritor.auth.AuthInterceptor
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import qq.service.QQInterfaces
@ -46,4 +52,15 @@ object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() {
msg = "Invalid ticket"
}
}
@Grpc("Authentication", "GetAuthState")
override suspend fun getAuthState(request: GetAuthStateReq): GetAuthStateRsp {
if (request.account != QQInterfaces.app.account) {
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
}
return getAuthStateRsp {
isRequiredAuth = AuthInterceptor.getAllTicket().isNotEmpty()
}
}
}

View File

@ -1,18 +1,37 @@
package kritor.service
import android.os.Bundle
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.*
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.qroute.QRoute
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.AuthCode
import io.kritor.authRsp
import io.kritor.contact.ContactServiceGrpcKt
import io.kritor.contact.GetUidRequest
import io.kritor.contact.GetUidResponse
import io.kritor.contact.GetUinByUidRequest
import io.kritor.contact.GetUinByUidResponse
import io.kritor.contact.IsBlackListUserRequest
import io.kritor.contact.IsBlackListUserResponse
import io.kritor.contact.ProfileCard
import io.kritor.contact.ProfileCardRequest
import io.kritor.contact.SetProfileCardRequest
import io.kritor.contact.SetProfileCardResponse
import io.kritor.contact.StrangerExt
import io.kritor.contact.StrangerInfo
import io.kritor.contact.StrangerInfoRequest
import io.kritor.contact.profileCard
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import io.kritor.contact.strangerInfo
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
@Grpc("ContactService", "GetProfileCard")
override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard {
val uin = if (request.hasUin()) request.uin
else ContactHelper.getUinByUidAsync(request.uid).toLong()
@ -44,4 +63,96 @@ object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
.withDescription("logic failed")
)
}
@Grpc("ContactService", "GetStrangerInfo")
override suspend fun getStrangerInfo(request: StrangerInfoRequest): StrangerInfo {
val userId = request.uin
val info = ContactHelper.refreshAndGetProfileCard(userId).onFailure {
throw StatusRuntimeException(Status.INTERNAL
.withCause(it)
.withDescription("Unable to fetch stranger info")
)
}.getOrThrow()
return strangerInfo {
this.uid = ContactHelper.getUidByUinAsync(userId)
this.uin = (info.uin ?: "0").toLong()
this.name = info.strNick ?: ""
this.level = info.iQQLevel
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = StrangerExt.newBuilder()
.setBigVip(info.bBigClubVipOpen == 1.toByte())
.setHollywoodVip(info.bHollywoodVipOpen == 1.toByte())
.setQqVip(info.bQQVipOpen == 1.toByte())
.setSuperVip(info.bSuperQQOpen == 1.toByte())
.setVoted(info.bVoted == 1.toByte())
.build().toByteString()
}
}
@Grpc("ContactService", "GetUid")
override suspend fun getUid(request: GetUidRequest): GetUidResponse {
return GetUidResponse.newBuilder().apply {
request.uinList.forEach {
putUid(it, ContactHelper.getUidByUinAsync(it))
}
}.build()
}
@Grpc("ContactService", "GetUinByUid")
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
return GetUinByUidResponse.newBuilder().apply {
request.uidList.forEach {
putUin(it, ContactHelper.getUinByUidAsync(it).toLong())
}
}.build()
}
@Grpc("ContactService", "SetProfileCard")
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
val bundle = Bundle()
val service = QQInterfaces.app
.getRuntimeService(IProfileProtocolService::class.java, "all")
if (request.hasNickName()) {
bundle.putString(KEY_NICK, request.nickName)
}
if (request.hasCompany()) {
bundle.putString(KEY_COMPANY, request.company)
}
if (request.hasEmail()) {
bundle.putString(KEY_EMAIL, request.email)
}
if (request.hasCollege()) {
bundle.putString(KEY_COLLEGE, request.college)
}
if (request.hasPersonalNote()) {
bundle.putString(KEY_PERSONAL_NOTE, request.personalNote)
}
if (request.hasBirthday()) {
bundle.putInt(KEY_BIRTHDAY, request.birthday)
}
if (request.hasAge()) {
bundle.putInt(KEY_AGE, request.age)
}
service.setProfileDetail(bundle)
return super.setProfileCard(request)
}
@Grpc("ContactService", "IsBlackListUser")
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
val isBlack = withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
blacklistApi.isBlackOrBlackedUin(request.uin.toString()) {
continuation.resume(it)
}
}
} ?: false
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
}
}

View File

@ -0,0 +1,118 @@
package kritor.service
import android.util.Base64
import com.tencent.mobileqq.app.QQAppInterface
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.core.ClearCacheRequest
import io.kritor.core.ClearCacheResponse
import io.kritor.core.DownloadFileRequest
import io.kritor.core.DownloadFileResponse
import io.kritor.core.GetCurrentAccountRequest
import io.kritor.core.GetCurrentAccountResponse
import io.kritor.core.GetVersionRequest
import io.kritor.core.GetVersionResponse
import io.kritor.core.KritorServiceGrpcKt
import io.kritor.core.SwitchAccountRequest
import io.kritor.core.SwitchAccountResponse
import io.kritor.core.clearCacheResponse
import io.kritor.core.downloadFileResponse
import io.kritor.core.getCurrentAccountResponse
import io.kritor.core.getVersionResponse
import io.kritor.core.switchAccountResponse
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import mqq.app.MobileQQ
import qq.service.QQInterfaces
import qq.service.QQInterfaces.Companion.app
import qq.service.contact.ContactHelper
import java.io.File
object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
@Grpc("KritorService", "GetVersion")
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
return getVersionResponse {
this.version = ShamrockVersion
this.appName = "Shamrock"
}
}
@Grpc("KritorService", "ClearCache")
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
FileUtils.clearCache()
MMKVFetcher.mmkvWithId("audio2silk")
.clear()
return clearCacheResponse {}
}
@Grpc("KritorService", "GetCurrentAccount")
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
return getCurrentAccountResponse {
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
this.accountUid = app.currentUid ?: ""
this.accountUin = (app.currentUin ?: "0").toLong()
}
}
@Grpc("KritorService", "DownloadFile")
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
val headerMap = mutableMapOf(
"User-Agent" to "Shamrock"
)
if (request.hasHeaders()) {
request.headers.split("[\r\n]").forEach {
val pair = it.split("=")
if (pair.size >= 2) {
val (k, v) = pair
headerMap[k] = v
}
}
}
var tmp = FileUtils.getTmpFile("cache")
if (request.hasBase64()) {
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
tmp.writeBytes(bytes)
} else if(request.hasUrl()) {
if(!DownloadUtils.download(
urlAdr = request.url,
dest = tmp,
headers = headerMap,
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
)) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed"))
}
}
tmp = if (!request.hasFileName()) FileUtils.renameByMd5(tmp)
else tmp.parentFile!!.resolve(request.fileName).also {
tmp.renameTo(it)
}
if (request.hasRootPath()) {
tmp = File(request.rootPath).resolve(tmp.name).also {
tmp.renameTo(it)
}
}
return downloadFileResponse {
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
this.fileAbsolutePath = tmp.absolutePath
}
}
@Grpc("KritorService", "SwitchAccount")
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
val uin = if (request.hasAccountUin()) request.accountUin.toString()
else ContactHelper.getUinByUidAsync(request.accountUid)
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
runCatching {
app.switchAccount(account, null)
}.onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
}
return switchAccountResponse { }
}
}

View File

@ -33,7 +33,7 @@ object DownloadUtils {
threadCount: Int = MAX_THREAD,
headers: Map<String, String> = mapOf()
): Boolean {
var threadCnt = if(threadCount == 0) MAX_THREAD else threadCount
var threadCnt = if(threadCount == 0 || threadCount < 0) MAX_THREAD else threadCount
val url = URL(urlAdr)
val connection = withContext(Dispatchers.IO) { url.openConnection() } as HttpURLConnection
headers.forEach { (k, v) ->