feat: 获取 `z_id`

This commit is contained in:
HuanCheng65 2023-07-19 16:46:21 +08:00
parent 035102dcf3
commit 2ae4d728ee
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
10 changed files with 285 additions and 56 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="tblite" />
<version value="33" />
<version value="35" />
<list>
<mapping class="com.huanchengfly.tieba.post.models.database.Account" />
<mapping class="com.huanchengfly.tieba.post.models.database.Draft" />

View File

@ -75,6 +75,8 @@ fun Any.toJson(): String = Gson().toJson(this)
fun String.toMD5(): String = MD5Util.toMd5(this)
fun ByteArray.toMD5(): String = MD5Util.toMd5(this)
fun Context.getColorCompat(@ColorRes id: Int): Int {
return ContextCompat.getColor(this, id)
}

View File

@ -0,0 +1,19 @@
package com.huanchengfly.tieba.post.api.models
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SofireResponse(
val data: String,
@SerialName("request_id")
@SerializedName("request_id")
val requestId: Long,
val skey: String
)
@Serializable
data class SofireResponseData(
val token: String
)

View File

@ -25,6 +25,7 @@ import com.huanchengfly.tieba.post.api.retrofit.interfaces.MiniTiebaApi
import com.huanchengfly.tieba.post.api.retrofit.interfaces.NewTiebaApi
import com.huanchengfly.tieba.post.api.retrofit.interfaces.OfficialProtobufTiebaApi
import com.huanchengfly.tieba.post.api.retrofit.interfaces.OfficialTiebaApi
import com.huanchengfly.tieba.post.api.retrofit.interfaces.SofireApi
import com.huanchengfly.tieba.post.api.retrofit.interfaces.WebTiebaApi
import com.huanchengfly.tieba.post.toJson
import com.huanchengfly.tieba.post.utils.AccountUtil
@ -224,6 +225,21 @@ object RetrofitTiebaApi {
)
}
val SOFIRE_API: SofireApi by lazy {
Retrofit.Builder()
.baseUrl("https://sofire.baidu.com/")
.addCallAdapterFactory(DeferredCallAdapterFactory())
.addCallAdapterFactory(FlowCallAdapterFactory.create())
.addConverterFactory(NullOnEmptyConverterFactory())
.addConverterFactory(gsonConverterFactory)
.client(OkHttpClient.Builder().apply {
// addInterceptor()
connectionPool(connectionPool)
}.build())
.build()
.create(SofireApi::class.java)
}
private inline fun <reified T : Any> createJsonApi(
baseUrl: String,
vararg interceptors: Interceptor

View File

@ -0,0 +1,21 @@
package com.huanchengfly.tieba.post.api.retrofit.interfaces
import com.huanchengfly.tieba.post.api.models.SofireResponse
import kotlinx.coroutines.flow.Flow
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.HeaderMap
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface SofireApi {
@POST
// @FormUrlEncoded
fun post(
@Url url: String,
@Query("skey") skey: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>,
): Flow<SofireResponse>
}

View File

@ -25,6 +25,8 @@ data class Account @JvmOverloads constructor(
var birthdayTime: String? = null,
var constellation: String? = null,
var loadSuccess: Boolean = false,
var uuid: String? = "",
var zid: String? = ""
) : LitePalSupport() {
val id: Int = 0
}

View File

@ -12,13 +12,20 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.staticCompositionLocalOf
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.InitNickNameBean
import com.huanchengfly.tieba.post.api.models.LoginBean
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.models.database.Account
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.zip
import org.litepal.LitePal
import org.litepal.LitePal.findAll
import org.litepal.LitePal.where
import org.litepal.extension.findAllAsync
import org.litepal.extension.findFirst
import java.util.UUID
object AccountUtil {
const val TAG = "AccountUtil"
@ -67,6 +74,11 @@ object AccountUtil {
return currentAccount
}
@JvmStatic
fun <T> getAccountInfo(getter: Account.() -> T): T? {
return currentAccount?.getter()
}
fun newAccount(uid: String, account: Account, callback: (Boolean) -> Unit) {
account.saveOrUpdateAsync("uid = ?", uid).listen {
mutableAllAccountsState.value = findAll(Account::class.java)
@ -80,7 +92,7 @@ object AccountUtil {
@JvmStatic
fun getAccountInfoByUid(uid: String): Account? {
return where("uid = ?", uid).findFirst(Account::class.java)
return where("uid = ?", uid).findFirst<Account>()
}
@JvmStatic
@ -103,44 +115,23 @@ object AccountUtil {
.putInt("now", id).commit()
}
fun fetchAccountFlow(): Flow<Account> {
return TiebaApi.getInstance()
.initNickNameFlow()
.zip(TiebaApi.getInstance().loginFlow()) { initNickNameBean, loginBean ->
getLoginInfo()!!.apply {
uid = loginBean.user.id
name = loginBean.user.name
nameShow = initNickNameBean.userInfo.nameShow
portrait = loginBean.user.portrait
tbs = loginBean.anti.tbs
saveOrUpdate("uid = ?", loginBean.user.id)
mutableAllAccountsState.value = findAll(Account::class.java)
}
}
private fun updateAccount(
account: Account,
initNickNameBean: InitNickNameBean,
loginBean: LoginBean,
) {
account.apply {
uid = loginBean.user.id
name = loginBean.user.name
nameShow = initNickNameBean.userInfo.nameShow
portrait = loginBean.user.portrait
tbs = loginBean.anti.tbs
if (uuid.isNullOrBlank()) uuid = UUID.randomUUID().toString()
}
}
fun fetchAccountFlow(account: Account): Flow<Account> {
return TiebaApi.getInstance()
.initNickNameFlow(
account.bduss,
account.sToken
)
.zip(
TiebaApi.getInstance().loginFlow(
account.bduss,
account.sToken
)
) { initNickNameBean, loginBean ->
account.apply {
uid = loginBean.user.id
name = loginBean.user.name
nameShow = initNickNameBean.userInfo.nameShow
portrait = loginBean.user.portrait
tbs = loginBean.anti.tbs
saveOrUpdate("uid = ?", loginBean.user.id)
mutableAllAccountsState.value = findAll(Account::class.java)
}
}
fun fetchAccountFlow(account: Account = getLoginInfo()!!): Flow<Account> {
return fetchAccountFlow(account.bduss, account.sToken, account.cookie)
}
fun fetchAccountFlow(
@ -154,25 +145,34 @@ object AccountUtil {
getAccountInfoByUid(loginBean.user.id)?.apply {
this.bduss = bduss
this.sToken = sToken
this.tbs = loginBean.anti.tbs
this.name = loginBean.user.name
this.nameShow = initNickNameBean.userInfo.nameShow
this.portrait = loginBean.user.portrait
this.cookie = cookie ?: getBdussCookie(bduss)
saveOrUpdate("uid = ?", loginBean.user.id)
}
?: Account(
loginBean.user.id,
loginBean.user.name,
bduss,
loginBean.anti.tbs,
loginBean.user.portrait,
sToken,
cookie ?: getBdussCookie(bduss),
initNickNameBean.userInfo.nameShow,
"",
"0"
)
updateAccount(this, initNickNameBean, loginBean)
} ?: Account(
loginBean.user.id,
loginBean.user.name,
bduss,
loginBean.anti.tbs,
loginBean.user.portrait,
sToken,
cookie ?: getBdussCookie(bduss),
initNickNameBean.userInfo.nameShow,
"",
"0"
)
}
.zip(SofireUtils.fetchZid()) { account, zid ->
account.apply { this.zid = zid }
}
.onEach { account ->
account.updateAllAsync("uid = ?", account.uid)
.listen { rowAffected ->
if (rowAffected > 0) {
LitePal.findAllAsync<Account>()
.listen {
mutableAllAccountsState.value = it
}
}
}
}
}

View File

@ -0,0 +1,10 @@
package com.huanchengfly.tieba.post.utils
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPOutputStream
fun String.gzipCompress(): ByteArray {
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(this) }
return bos.toByteArray()
}

View File

@ -0,0 +1,67 @@
package com.huanchengfly.tieba.post.utils
import kotlin.experimental.xor
class RC442 {
private var x: Int = 0
private var y: Int = 0
private var m: ByteArray = ByteArray(256)
fun setup(key: ByteArray, keyLen: Int = key.size) {
var i: Int
var j: Int
var a: Int
for (i in 0 until 256) {
m[i] = i.toByte()
}
j = 0
var k = 0
for (i in 0 until 256) {
if (k >= keyLen)
k = 0
a = m[i].toInt()
j = (j + a + key[k].toInt()) and 0xFF
m[i] = m[j]
m[j] = a.toByte()
k++
}
}
fun crypt(src: ByteArray, srcLen: Int = src.size): ByteArray {
val dst = ByteArray(src.size)
var x = this.x
var y = this.y
for (i in 0 until srcLen) {
x = (x + 1) and 0xFF
val a = m[x].toInt()
y = (y + a) and 0xFF
val b = m[y].toInt()
m[x] = b.toByte()
m[y] = a.toByte()
dst[i] = (src[i] xor m[(a + b) and 0xFF]) xor 42.toByte()
}
this.x = x
this.y = y
return dst
}
}
fun rc442Crypt(
src: ByteArray,
key: ByteArray,
srcLen: Int = src.size,
keyLen: Int = key.size
): ByteArray {
val rc442 = RC442()
rc442.setup(key, keyLen)
return rc442.crypt(src, srcLen)
}

View File

@ -0,0 +1,92 @@
package com.huanchengfly.tieba.post.utils
import android.util.Base64
import com.huanchengfly.tieba.post.api.models.SofireResponseData
import com.huanchengfly.tieba.post.api.retrofit.RetrofitTiebaApi
import com.huanchengfly.tieba.post.toMD5
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import java.security.MessageDigest
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
@Serializable
data class SofireRequestBody(
@SerialName("module_section")
val moduleSection: List<Map<String, String>>
)
fun generateRandomString(length: Int): String {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return (1..length)
.map { Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
}
object SofireUtils {
const val DEFAULT_APP_KEY = "200033"
const val DEFAULT_SECRET_KEY = "ea737e4f435b53786043369d2e5ace4f"
fun fetchZid(): Flow<String> {
val appKey = DEFAULT_APP_KEY
val secKey = DEFAULT_SECRET_KEY
val cuid = "${UIDUtil.uUID.toMD5().uppercase()}|0"
val cuidMd5 = cuid.toMD5().lowercase()
val currTime = "${System.currentTimeMillis() / 1000}"
val reqBody =
Json.encodeToString(SofireRequestBody(listOf(mapOf("zid" to cuid)))).gzipCompress()
val randomKey = generateRandomString(16)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val iv = IvParameterSpec(0.toChar().toString().repeat(16).encodeToByteArray())
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(randomKey.toByteArray(), "AES"), iv)
val encBody = cipher.doFinal(reqBody)
val reqBodyMd5Digest = MessageDigest.getInstance("MD5").apply { update(reqBody) }.digest()
val finalBody =
(encBody + reqBodyMd5Digest).toRequestBody("application/x-www-form-urlencoded".toMediaType())
val headers = mapOf(
"Pragma" to "no-cache",
"Accept" to "*/*",
"Accept-Language" to Locale.getDefault().language,
"x-device-id" to cuidMd5,
"x-client-src" to "src",
"User-Agent" to "x6/$appKey/12.35.1.0/4.4.1.3",
"x-sdk-ver" to "sofire/3.5.9.6",
"x-plu-ver" to "x6/4.4.1.3",
"x-app-ver" to "com.baidu.tieba/12.35.1.0",
"x-api-ver" to "33"
)
val pathMd5 = listOf(appKey, currTime, secKey).joinToString("").toMD5().lowercase()
val skey = Base64.encodeToString(
rc442Crypt(randomKey.encodeToByteArray(), cuidMd5.encodeToByteArray(), 16, 32),
Base64.DEFAULT
)
val url = "https://sofire.baidu.com/c/11/z/100/$appKey/$currTime/$pathMd5"
return RetrofitTiebaApi.SOFIRE_API
.post(url, skey, finalBody, headers)
.map {
val resSkey = rc442Crypt(
Base64.decode(it.skey, Base64.DEFAULT),
cuidMd5.encodeToByteArray(),
16,
32
)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(resSkey, "AES"), iv)
val decode = Base64.decode(it.data, Base64.DEFAULT).dropLast(16).toByteArray()
val json = Json { ignoreUnknownKeys = true }
val decryptData = json.decodeFromString<SofireResponseData>(
cipher.doFinal(decode).decodeToString()
)
decryptData.token
}
}
}