feat: 获取 `z_id`
This commit is contained in:
parent
035102dcf3
commit
2ae4d728ee
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<litepal>
|
<litepal>
|
||||||
<dbname value="tblite" />
|
<dbname value="tblite" />
|
||||||
<version value="33" />
|
<version value="35" />
|
||||||
<list>
|
<list>
|
||||||
<mapping class="com.huanchengfly.tieba.post.models.database.Account" />
|
<mapping class="com.huanchengfly.tieba.post.models.database.Account" />
|
||||||
<mapping class="com.huanchengfly.tieba.post.models.database.Draft" />
|
<mapping class="com.huanchengfly.tieba.post.models.database.Draft" />
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ fun Any.toJson(): String = Gson().toJson(this)
|
||||||
|
|
||||||
fun String.toMD5(): String = MD5Util.toMd5(this)
|
fun String.toMD5(): String = MD5Util.toMd5(this)
|
||||||
|
|
||||||
|
fun ByteArray.toMD5(): String = MD5Util.toMd5(this)
|
||||||
|
|
||||||
fun Context.getColorCompat(@ColorRes id: Int): Int {
|
fun Context.getColorCompat(@ColorRes id: Int): Int {
|
||||||
return ContextCompat.getColor(this, id)
|
return ContextCompat.getColor(this, id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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.NewTiebaApi
|
||||||
import com.huanchengfly.tieba.post.api.retrofit.interfaces.OfficialProtobufTiebaApi
|
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.OfficialTiebaApi
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.interfaces.SofireApi
|
||||||
import com.huanchengfly.tieba.post.api.retrofit.interfaces.WebTiebaApi
|
import com.huanchengfly.tieba.post.api.retrofit.interfaces.WebTiebaApi
|
||||||
import com.huanchengfly.tieba.post.toJson
|
import com.huanchengfly.tieba.post.toJson
|
||||||
import com.huanchengfly.tieba.post.utils.AccountUtil
|
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(
|
private inline fun <reified T : Any> createJsonApi(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
vararg interceptors: Interceptor
|
vararg interceptors: Interceptor
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
|
@ -25,6 +25,8 @@ data class Account @JvmOverloads constructor(
|
||||||
var birthdayTime: String? = null,
|
var birthdayTime: String? = null,
|
||||||
var constellation: String? = null,
|
var constellation: String? = null,
|
||||||
var loadSuccess: Boolean = false,
|
var loadSuccess: Boolean = false,
|
||||||
|
var uuid: String? = "",
|
||||||
|
var zid: String? = ""
|
||||||
) : LitePalSupport() {
|
) : LitePalSupport() {
|
||||||
val id: Int = 0
|
val id: Int = 0
|
||||||
}
|
}
|
||||||
|
|
@ -12,13 +12,20 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.api.TiebaApi
|
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.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
|
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.models.database.Account
|
import com.huanchengfly.tieba.post.models.database.Account
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.zip
|
import kotlinx.coroutines.flow.zip
|
||||||
|
import org.litepal.LitePal
|
||||||
import org.litepal.LitePal.findAll
|
import org.litepal.LitePal.findAll
|
||||||
import org.litepal.LitePal.where
|
import org.litepal.LitePal.where
|
||||||
|
import org.litepal.extension.findAllAsync
|
||||||
|
import org.litepal.extension.findFirst
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
object AccountUtil {
|
object AccountUtil {
|
||||||
const val TAG = "AccountUtil"
|
const val TAG = "AccountUtil"
|
||||||
|
|
@ -67,6 +74,11 @@ object AccountUtil {
|
||||||
return currentAccount
|
return currentAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun <T> getAccountInfo(getter: Account.() -> T): T? {
|
||||||
|
return currentAccount?.getter()
|
||||||
|
}
|
||||||
|
|
||||||
fun newAccount(uid: String, account: Account, callback: (Boolean) -> Unit) {
|
fun newAccount(uid: String, account: Account, callback: (Boolean) -> Unit) {
|
||||||
account.saveOrUpdateAsync("uid = ?", uid).listen {
|
account.saveOrUpdateAsync("uid = ?", uid).listen {
|
||||||
mutableAllAccountsState.value = findAll(Account::class.java)
|
mutableAllAccountsState.value = findAll(Account::class.java)
|
||||||
|
|
@ -80,7 +92,7 @@ object AccountUtil {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getAccountInfoByUid(uid: String): Account? {
|
fun getAccountInfoByUid(uid: String): Account? {
|
||||||
return where("uid = ?", uid).findFirst(Account::class.java)
|
return where("uid = ?", uid).findFirst<Account>()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
@ -103,44 +115,23 @@ object AccountUtil {
|
||||||
.putInt("now", id).commit()
|
.putInt("now", id).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAccountFlow(): Flow<Account> {
|
private fun updateAccount(
|
||||||
return TiebaApi.getInstance()
|
account: Account,
|
||||||
.initNickNameFlow()
|
initNickNameBean: InitNickNameBean,
|
||||||
.zip(TiebaApi.getInstance().loginFlow()) { initNickNameBean, loginBean ->
|
loginBean: 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
account.apply {
|
||||||
uid = loginBean.user.id
|
uid = loginBean.user.id
|
||||||
name = loginBean.user.name
|
name = loginBean.user.name
|
||||||
nameShow = initNickNameBean.userInfo.nameShow
|
nameShow = initNickNameBean.userInfo.nameShow
|
||||||
portrait = loginBean.user.portrait
|
portrait = loginBean.user.portrait
|
||||||
tbs = loginBean.anti.tbs
|
tbs = loginBean.anti.tbs
|
||||||
saveOrUpdate("uid = ?", loginBean.user.id)
|
if (uuid.isNullOrBlank()) uuid = UUID.randomUUID().toString()
|
||||||
mutableAllAccountsState.value = findAll(Account::class.java)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchAccountFlow(account: Account = getLoginInfo()!!): Flow<Account> {
|
||||||
|
return fetchAccountFlow(account.bduss, account.sToken, account.cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchAccountFlow(
|
fun fetchAccountFlow(
|
||||||
|
|
@ -154,14 +145,9 @@ object AccountUtil {
|
||||||
getAccountInfoByUid(loginBean.user.id)?.apply {
|
getAccountInfoByUid(loginBean.user.id)?.apply {
|
||||||
this.bduss = bduss
|
this.bduss = bduss
|
||||||
this.sToken = sToken
|
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)
|
this.cookie = cookie ?: getBdussCookie(bduss)
|
||||||
saveOrUpdate("uid = ?", loginBean.user.id)
|
updateAccount(this, initNickNameBean, loginBean)
|
||||||
}
|
} ?: Account(
|
||||||
?: Account(
|
|
||||||
loginBean.user.id,
|
loginBean.user.id,
|
||||||
loginBean.user.name,
|
loginBean.user.name,
|
||||||
bduss,
|
bduss,
|
||||||
|
|
@ -174,6 +160,20 @@ object AccountUtil {
|
||||||
"0"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue