diff --git a/app/src/main/java/com/github/miwu/ui/device/DeviceActivity.kt b/app/src/main/java/com/github/miwu/ui/device/DeviceActivity.kt index 2b55d1a2..dd73e75b 100644 --- a/app/src/main/java/com/github/miwu/ui/device/DeviceActivity.kt +++ b/app/src/main/java/com/github/miwu/ui/device/DeviceActivity.kt @@ -31,8 +31,10 @@ import com.github.miwu.databinding.ActivityDeviceBinding as Binding class DeviceActivity : ViewActivityX(Binding::inflate), DeviceManagerCallback { override val viewModel: DeviceViewModel by viewModel() - private val device by lazy { intent.getStringExtra("device")!!.to() } - private val user by lazy { intent.getStringExtra("user")!!.to() } + + // 如果确认接收的Extra是符合预期的这里可以直接Unwrap + private val device by lazy { intent.getStringExtra("device")!!.to().getOrThrow() } + private val user by lazy { intent.getStringExtra("user")!!.to().getOrThrow() } private val logger = Logger() private val miotDeviceClient by lazy { MiotDeviceClient(user) } private val specAttrProvider: MiotSpecAttrProvider by inject() @@ -149,7 +151,7 @@ class DeviceActivity : ViewActivityX(Binding::inflate), DeviceManagerCa return if (!file.isFile) { return null } else { - file.readText().to() + file.readText().to().getOrThrow() } } catch (e: Exception) { return null @@ -167,7 +169,7 @@ class DeviceActivity : ViewActivityX(Binding::inflate), DeviceManagerCa return if (!file.isFile) { return null } else { - file.readText().to>() + file.readText().to>().getOrThrow() } } catch (e: Exception) { return null diff --git a/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotLoginProviderImpl.kt b/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotLoginProviderImpl.kt index 020fa6ac..bf00c681 100644 --- a/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotLoginProviderImpl.kt +++ b/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotLoginProviderImpl.kt @@ -47,12 +47,9 @@ class MiotLoginProviderImpl : MiotLoginProvider { readTimeout(5, TimeUnit.MINUTES) } - override suspend fun login( - user: String, - pwd: String - ) = runCatching { + override suspend fun login(user: String, pwd: String): Result = runCatching { cookieJar.clear() - val sidDetails = getLocation() + val sidDetails = getLocation().getOrThrow() val pwdHash = pwd.md5() val body = FormBody { add("qs", sidDetails.qs) @@ -64,18 +61,22 @@ class MiotLoginProviderImpl : MiotLoginProvider { add("_json", "true") } get(SERVICE_LOGIN_AUTH_URL, body) + .getOrThrow() .to() + .getOrThrow() .execute() .getOrThrow() } - override suspend fun loginByQrCode(loginUrl: String) = runCatching { + override suspend fun loginByQrCode(loginUrl: String): Result = runCatching { cookieJar.clear() get(loginUrl) + .getOrThrow() .removePrefix() .to() + .getOrThrow() .also { - val (location, securityToken) = getServiceData() + val (location, securityToken) = getServiceData().getOrThrow() it.location = location it.ssecurity = securityToken } @@ -83,7 +84,6 @@ class MiotLoginProviderImpl : MiotLoginProvider { .getOrThrow() } - override suspend fun loginByQrCode( loginUrl: String, onSuccess: suspend CoroutineScope.(MiotUser) -> Unit, @@ -94,10 +94,12 @@ class MiotLoginProviderImpl : MiotLoginProvider { cookieJar.clear() try { get(loginUrl) + .getOrThrow() .removePrefix() .to() + .getOrThrow() .also { - val (location, securityToken) = getServiceData() + val (location, securityToken) = getServiceData().getOrThrow() it.location = location it.ssecurity = securityToken } @@ -115,7 +117,8 @@ class MiotLoginProviderImpl : MiotLoginProvider { } } - override suspend fun generateLoginQrCode(): Result = withContext(Dispatchers.IO) { + // 为啥这里要在IO上下文执行? + override suspend fun generateLoginQrCode(): Result = runCatching { val generateQrCode = """ ${QRCODE_GENERATE_URL}? ${ @@ -130,19 +133,18 @@ class MiotLoginProviderImpl : MiotLoginProvider { """.trimIndent().urlEncode() } """.trimIndent() - runCatching { - get(generateQrCode) - .removePrefix() - .to() - } + get(generateQrCode) + .getOrThrow() + .removePrefix() + .to() + .getOrThrow() } - override suspend fun refreshServiceToken(miotUser: MiotUser) = runCatching { + override suspend fun refreshServiceToken(miotUser: MiotUser): Result = runCatching { cookieJar.clear() - val url = HttpUrl.Builder() - .host("https://account.xiaomi.com") - .build() - with(miotUser) { + // 因为SimpleCookieJar的Store和Url无关,如果是手动添加Cookie是否不需要添加Url了呢 + // 所以添加一个手动实现addAll函数表示是手动添加Cookie + val cookies = with(miotUser) { listOf( Cookie("userId", userId), Cookie("cUserId", cUserId), @@ -151,14 +153,14 @@ class MiotLoginProviderImpl : MiotLoginProvider { Cookie("psecurity", passToken), Cookie("passToken", passToken), ) - }.let { cookieJar.saveFromResponse(url, it) } - val data = getLocation() - data.toException()?.let { throw it } + } + cookieJar.addAll(cookies) + val data = getLocation().getOrThrow() val location = data.location val serviceToken = getServiceToken(location).getOrThrow() miotUser.copy( ssecurity = data.ssecurity, - serviceToken = serviceToken + serviceToken = serviceToken, ) } @@ -183,15 +185,13 @@ class MiotLoginProviderImpl : MiotLoginProvider { ) } - private suspend fun getServiceToken(location: String) = runCatching { - val response = try { - get(location) - } catch (e: TimeoutException) { - throw MiotTimeoutException("Login", e) - } catch (e: IOException) { - throw MiotConnectionException("Login", e) - } catch (e: Exception) { - throw MiotHttpException("Login", e) + private suspend fun getServiceToken(location: String): Result = runCatching { + val response = get(location).getOrElse { e -> + when (e) { + is TimeoutException -> throw MiotTimeoutException("Login", e) + is IOException -> throw MiotConnectionException("Login", e) + else -> throw MiotHttpException("Login", e) + } } val cookiesHeader = response.headers["Set-Cookie"]!! cookiesHeader.split(", ").firstNotNullOfOrNull { cookieString -> @@ -204,19 +204,26 @@ class MiotLoginProviderImpl : MiotLoginProvider { } ?: throw MiotAuthException.tokenMissing() } - private suspend fun getLocation() = + private suspend fun getLocation(): Result = runCatching { + // 如果Location的code不是预期的是否可以在这里就把结果包装成异常 get(SERVICE_LOGIN_URL) + .getOrThrow() .removePrefix() .to() + .getOrThrow() + .getOrThrowAuthException() + } - private suspend fun getServiceData() = + private suspend fun getServiceData(): Result = runCatching { get(SERVICE_LOGIN_URL) + .getOrThrow() .removePrefix() .to() + .getOrThrow() + } - private suspend inline fun get( - url: String, body: RequestBody? = null - ): T = miotLoginClient.get(url, body) + private suspend inline fun get(url: String, body: RequestBody? = null): Result = + miotLoginClient.get(url, body) class SimpleCookieJar : CookieJar { private val storage = arrayListOf() @@ -225,7 +232,9 @@ class MiotLoginProviderImpl : MiotLoginProvider { storage.addAll(cookies) } - override fun loadForRequest(url: HttpUrl) = storage + override fun loadForRequest(url: HttpUrl): List = storage + + fun addAll(cookies: List) = storage.addAll(cookies) fun clear() = storage.clear() } diff --git a/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotSpecAttrProviderImpl.kt b/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotSpecAttrProviderImpl.kt index 2cbc04f9..750f0839 100644 --- a/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotSpecAttrProviderImpl.kt +++ b/miot-api-impl/src/main/java/miwu/miot/impl/provider/MiotSpecAttrProviderImpl.kt @@ -79,7 +79,7 @@ class MiotSpecAttrProviderImpl : MiotSpecAttrProvider { override suspend fun getIconUrl(model: String) = withContext(Dispatchers.IO) { runCatching { val url = "https://home.mi.com/cgi-op/api/v1/baike/v2/product?model=${model}" - val info = client.get(url) + val info = client.get(url).getOrThrow() if (info.code != 0) throw MiotClientException.getIconUrlFailed(model) info.data.realIcon } diff --git a/miot-api-impl/src/main/java/miwu/miot/utils/Crypto.kt b/miot-api-impl/src/main/java/miwu/miot/utils/Crypto.kt index 1a4487d3..2e550f78 100644 --- a/miot-api-impl/src/main/java/miwu/miot/utils/Crypto.kt +++ b/miot-api-impl/src/main/java/miwu/miot/utils/Crypto.kt @@ -4,7 +4,9 @@ import okio.ByteString.Companion.encodeUtf8 import java.net.URLEncoder import kotlin.io.encoding.Base64 -inline fun String.to(): T = json.decodeFromString(this) +inline fun String.to(): Result = runCatching { + json.decodeFromString(this) +} fun String.md5() = this.encodeUtf8() .md5() diff --git a/miot-api-impl/src/main/java/miwu/miot/utils/OkHttp.kt b/miot-api-impl/src/main/java/miwu/miot/utils/OkHttp.kt index 62701650..a1b0c0a9 100644 --- a/miot-api-impl/src/main/java/miwu/miot/utils/OkHttp.kt +++ b/miot-api-impl/src/main/java/miwu/miot/utils/OkHttp.kt @@ -11,7 +11,7 @@ import okio.Buffer import java.nio.charset.Charset -fun OkHttpClient.Builder.userAgent(ua: String) = addInterceptor { chain -> +fun OkHttpClient.Builder.userAgent(ua: String): OkHttpClient.Builder = addInterceptor { chain -> chain.proceed( chain.request() .newBuilder() @@ -21,37 +21,29 @@ fun OkHttpClient.Builder.userAgent(ua: String) = addInterceptor { chain -> ) } -fun OkHttpClient(block: OkHttpClient.Builder.() -> Unit = {}) = +fun OkHttpClient(block: OkHttpClient.Builder.() -> Unit = {}): OkHttpClient = OkHttpClient.Builder().apply(block).build() internal suspend inline fun OkHttpClient.get( url: String, - body: RequestBody? = null -): T = withContext(Dispatchers.IO) { - run { - newCall( - Request.Builder().url(url).apply { - if (body != null) post(body) - }.build() - ).execute().use { response -> - when (T::class) { - String::class -> response.body.string() as T + body: RequestBody? = null, +): Result = withContext(Dispatchers.IO) { + runCatching { + val request = Request.Builder() + .url(url) + .apply { if (body != null) post(body) } + .build() + newCall(request).execute().use { response -> + return@runCatching when (T::class) { Response::class -> response as T - else -> - try { - response.body.string().to() - } catch (e: Exception) { - throw IllegalArgumentException( - "Unsupported type: ${T::class.simpleName}", - e - ) - } + String::class -> response.body.string() as T + else -> response.body.string().to().getOrThrow() } } } } -fun FormBody(block: FormBody.Builder.() -> Unit) = FormBody.Builder().apply(block).build() +fun FormBody(block: FormBody.Builder.() -> Unit): FormBody = FormBody.Builder().apply(block).build() fun RequestBody.readToString(): String { Buffer().apply { @@ -68,6 +60,9 @@ fun RequestBody.readToString(): String { throw RuntimeException("data of requestBody is empty") } - -fun Request.Builder.addHeaders(vararg headers: Pair) = - this.also { builder -> headers.forEach { addHeader(it.first, it.second) } } \ No newline at end of file +fun Request.Builder.addHeaders(vararg headers: Pair): Request.Builder { + for ((k, v) in headers) { + addHeader(k, v) + } + return this +} diff --git a/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/impl/provider/MiotLoginProviderImpl.kt b/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/impl/provider/MiotLoginProviderImpl.kt index ac0ccb44..50e59ccc 100644 --- a/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/impl/provider/MiotLoginProviderImpl.kt +++ b/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/impl/provider/MiotLoginProviderImpl.kt @@ -8,7 +8,6 @@ import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.cookies.CookiesStorage import io.ktor.client.plugins.cookies.HttpCookies -import io.ktor.client.plugins.timeout import io.ktor.client.request.forms.formData import io.ktor.client.request.get import io.ktor.client.request.setBody @@ -23,26 +22,26 @@ import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import miwu.miot.kmp.utils.IO -import miwu.miot.kmp.utils.json -import miwu.miot.kmp.utils.md5 -import miwu.miot.kmp.utils.to import miwu.miot.common.MIOT_SID import miwu.miot.common.QRCODE_GENERATE_URL import miwu.miot.common.SERVICE_LOGIN_AUTH_URL import miwu.miot.common.SERVICE_LOGIN_URL +import miwu.miot.common.getRandomDeviceId +import miwu.miot.common.removePrefix import miwu.miot.exception.MiotAuthException import miwu.miot.exception.MiotBusinessException import miwu.miot.exception.MiotHttpException +import miwu.miot.kmp.utils.IO +import miwu.miot.kmp.utils.json +import miwu.miot.kmp.utils.md5 +import miwu.miot.kmp.utils.to import miwu.miot.model.MiotUser +import miwu.miot.model.login.Location import miwu.miot.model.login.Login import miwu.miot.model.login.LoginQrCode +import miwu.miot.model.login.ServiceData import miwu.miot.provider.MiotLoginProvider import kotlin.coroutines.CoroutineContext -import miwu.miot.common.getRandomDeviceId -import miwu.miot.common.removePrefix -import miwu.miot.model.login.ServiceData -import miwu.miot.model.login.Location import kotlin.time.Clock class MiotLoginProviderImpl : MiotLoginProvider { @@ -66,12 +65,9 @@ class MiotLoginProviderImpl : MiotLoginProvider { expectSuccess = true } - override suspend fun login( - user: String, - pwd: String - ) = runCatching { - cookiesStorage.close() - val sidDetails = getLocation() + override suspend fun login(user: String, pwd: String): Result = runCatching { + cookiesStorage.clear() + val sidDetails = getLocation().getOrThrow() val pwdHash = pwd.md5() val body = formData { append("qs", sidDetails.qs) @@ -83,18 +79,22 @@ class MiotLoginProviderImpl : MiotLoginProvider { append("_json", "true") } get(SERVICE_LOGIN_AUTH_URL, body) + .getOrThrow() .to() + .getOrThrow() .execute() .getOrThrow() } - override suspend fun loginByQrCode(loginUrl: String) = runCatching { - cookiesStorage.close() + override suspend fun loginByQrCode(loginUrl: String): Result = runCatching { + cookiesStorage.clear() get(loginUrl) + .getOrThrow() .removePrefix() .to() + .getOrThrow() .also { - val (location, securityToken) = getServiceData() + val (location, securityToken) = getServiceData().getOrThrow() it.location = location it.ssecurity = securityToken } @@ -109,13 +109,15 @@ class MiotLoginProviderImpl : MiotLoginProvider { onFailure: suspend CoroutineScope.(Throwable?) -> Unit, context: CoroutineContext ): Unit = withContext(Dispatchers.IO) { - cookiesStorage.close() + cookiesStorage.clear() try { get(loginUrl) + .getOrThrow() .removePrefix() .to() + .getOrThrow() .also { - val (location, securityToken) = getServiceData() + val (location, securityToken) = getServiceData().getOrThrow() it.location = location it.ssecurity = securityToken } @@ -133,7 +135,7 @@ class MiotLoginProviderImpl : MiotLoginProvider { } } - override suspend fun generateLoginQrCode() = withContext(Dispatchers.IO) { + override suspend fun generateLoginQrCode() = runCatching { val generateQrCode = """ ${QRCODE_GENERATE_URL}? ${ @@ -148,16 +150,16 @@ class MiotLoginProviderImpl : MiotLoginProvider { """.trimIndent().parseUrlEncodedParameters() } """.trimIndent() - runCatching { - get(generateQrCode) - .removePrefix() - .to() - } + get(generateQrCode) + .getOrThrow() + .removePrefix() + .to() + .getOrThrow() } override suspend fun refreshServiceToken(miotUser: MiotUser) = runCatching { - cookiesStorage.close() - with(miotUser) { + cookiesStorage.clear() + val cookies = with(miotUser) { listOf( Cookie("userId", userId), Cookie("cUserId", cUserId), @@ -166,14 +168,14 @@ class MiotLoginProviderImpl : MiotLoginProvider { Cookie("psecurity", passToken), Cookie("passToken", passToken), ) - }.forEach { cookiesStorage.addCookie(Url(""), it) } - val data = getLocation() - data.toException()?.let { throw it } + } + cookiesStorage.addAll(cookies) + val data = getLocation().getOrThrow() val location = data.location val serviceToken = getServiceToken(location).getOrThrow() miotUser.copy( ssecurity = data.ssecurity, - serviceToken = serviceToken + serviceToken = serviceToken, ) } @@ -206,21 +208,29 @@ class MiotLoginProviderImpl : MiotLoginProvider { ?: throw MiotAuthException.tokenMissing() } - private suspend fun getLocation() = + private suspend fun getLocation(): Result = runCatching { get(SERVICE_LOGIN_URL) + .getOrThrow() .removePrefix() .to() + .getOrThrow() + .getOrThrowAuthException() + } - private suspend fun getServiceData() = + private suspend fun getServiceData(): Result = runCatching { get(SERVICE_LOGIN_URL) + .getOrThrow() .removePrefix() .to() + .getOrThrow() + } private suspend inline fun get( - url: String, body: Any? = null - ): T = httpClient.get(url) { - setBody(body) - }.body() + url: String, + body: Any? = null, + ): Result = runCatching { + httpClient.get(url) { setBody(body) }.body() + } private class SimpleCookiesStorage : CookiesStorage { private val storage = mutableListOf() @@ -236,6 +246,11 @@ class MiotLoginProviderImpl : MiotLoginProvider { override fun close() { storage.clear() } + + // 添加和Url无关的手动修改Cookie的方法 + fun addAll(cookies: List) = storage.addAll(cookies) + + fun clear() = storage.clear() } fun String.splitSetCookieHeader(): List { diff --git a/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/utils/MiotCryptoUtil.kt b/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/utils/MiotCryptoUtil.kt index a3e33f47..16b2cec5 100644 --- a/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/utils/MiotCryptoUtil.kt +++ b/miot-api-kmp-impl/src/commonMain/kotlin/miwu/miot/kmp/utils/MiotCryptoUtil.kt @@ -2,7 +2,9 @@ package miwu.miot.kmp.utils import okio.ByteString.Companion.encodeUtf8 -inline fun String.to(): T = json.decodeFromString(this) +inline fun String.to(): Result = runCatching { + json.decodeFromString(this) +} fun String.md5() = this.encodeUtf8() .md5() diff --git a/miot-api/src/commonMain/kotlin/miwu/miot/model/login/Location.kt b/miot-api/src/commonMain/kotlin/miwu/miot/model/login/Location.kt index 7f1e9cc8..8aba8f53 100644 --- a/miot-api/src/commonMain/kotlin/miwu/miot/model/login/Location.kt +++ b/miot-api/src/commonMain/kotlin/miwu/miot/model/login/Location.kt @@ -14,15 +14,15 @@ data class Location( @SerialName("location") val location: String, @SerialName("ssecurity") val ssecurity: String ) { - fun toException(): Throwable? { + fun getOrThrowAuthException(): Location { return when (code) { - 20003 -> MiotAuthException("Invalid UserName") - 22009 -> MiotAuthException("Package Name Denied Exception") - 70002 -> MiotAuthException.invalidCredentials() - 70016 -> MiotAuthException.invalidCredentials() - 81003 -> MiotAuthException.needVerification() - 87001 -> MiotAuthException.needVerification() - else -> null // Not sure what else could happen here + 20003 -> throw MiotAuthException("Invalid UserName") + 22009 -> throw MiotAuthException("Package Name Denied Exception") + 70002 -> throw MiotAuthException.invalidCredentials() + 70016 -> throw MiotAuthException.invalidCredentials() + 81003 -> throw MiotAuthException.needVerification() + 87001 -> throw MiotAuthException.needVerification() + else -> this } } }