From c926467c96fc1678fc90d7d8af15cb1e87235c2e Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+antsylich@users.noreply.github.com> Date: Sun, 28 Jan 2024 01:12:18 +0700 Subject: [PATCH] fix: Refactor MAL code to not spam refresh token when it fails --- .../tachiyomi/data/track/TrackPreferences.kt | 6 ++ .../data/track/myanimelist/MyAnimeList.kt | 12 +++- .../data/track/myanimelist/MyAnimeListApi.kt | 37 +++++------ .../myanimelist/MyAnimeListInterceptor.kt | 66 +++++++++---------- 4 files changed, 67 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackPreferences.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackPreferences.kt index 1117bc5fb1..72942c17b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackPreferences.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackPreferences.kt @@ -12,9 +12,15 @@ class TrackPreferences( fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "") + fun trackAuthExpired(sync: TrackService) = preferenceStore.getBoolean( + Preference.privateKey("pref_tracker_auth_expired_${sync.id}"), + false, + ) + fun setCredentials(sync: TrackService, username: String, password: String) { trackUsername(sync).set(username) trackPassword(sync).set(password) + trackAuthExpired(sync).set(false) } fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "") diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt index 667da422f0..45226e71e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeList.kt @@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.updateNewTrackInfo -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import timber.log.Timber @@ -17,7 +16,7 @@ import uy.kohesive.injekt.injectLazy class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { private val json: Json by injectLazy() - private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) } + private val interceptor by lazy { MyAnimeListInterceptor(this) } private val api by lazy { MyAnimeListApi(client, interceptor) } @StringRes @@ -142,6 +141,14 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { interceptor.setAuth(null) } + fun getIfAuthExpired(): Boolean { + return trackPreferences.trackAuthExpired(this).get() + } + + fun setAuthExpired() { + trackPreferences.trackAuthExpired(this).set(true) + } + fun saveOAuth(oAuth: OAuth?) { trackPreferences.trackToken(this).set(json.encodeToString(oAuth)) } @@ -153,6 +160,7 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) { null } } + companion object { const val READING = 1 const val COMPLETED = 2 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 8092d45105..f80e2e1a4b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -41,13 +41,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI suspend fun getAccessToken(authCode: String): OAuth { return withIOContext { val formBody: RequestBody = FormBody.Builder() - .add("client_id", clientId) + .add("client_id", CLIENT_ID) .add("code", authCode) .add("code_verifier", codeVerifier) .add("grant_type", "authorization_code") .build() with(json) { - client.newCall(POST("$baseOAuthUrl/token", body = formBody)) + client.newCall(POST("$BASE_OAUTH_URL/token", body = formBody)) .awaitSuccess() .parseAs() } @@ -57,7 +57,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI suspend fun getCurrentUser(): String { return withIOContext { val request = Request.Builder() - .url("$baseApiUrl/users/@me") + .url("$BASE_API_URL/users/@me") .get() .build() with(json) { @@ -71,7 +71,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI suspend fun search(query: String): List { return withIOContext { - val url = "$baseApiUrl/manga".toUri().buildUpon() + val url = "$BASE_API_URL/manga".toUri().buildUpon() // MAL API throws a 400 when the query is over 64 characters... .appendQueryParameter("q", query.take(64)) .appendQueryParameter("nsfw", "true") @@ -96,7 +96,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI suspend fun getMangaDetails(id: Int): TrackSearch { return withIOContext { - val url = "$baseApiUrl/manga".toUri().buildUpon() + val url = "$BASE_API_URL/manga".toUri().buildUpon() .appendPath(id.toString()) .appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date") .build() @@ -156,7 +156,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI suspend fun findListItem(track: Track): Track? { return withIOContext { - val uri = "$baseApiUrl/manga".toUri().buildUpon() + val uri = "$BASE_API_URL/manga".toUri().buildUpon() .appendPath(track.media_id.toString()) .appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}") .build() @@ -194,7 +194,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI // Check next page if there's more if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) { - matches + findListItems(query, offset + listPaginationAmount) + matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT) } else { matches } @@ -203,9 +203,9 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI private suspend fun getListPage(offset: Int): JsonObject { return withIOContext { - val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon() + val urlBuilder = "$BASE_API_URL/users/@me/mangalist".toUri().buildUpon() .appendQueryParameter("fields", "list_status{start_date,finish_date}") - .appendQueryParameter("limit", listPaginationAmount.toString()) + .appendQueryParameter("limit", LIST_PAGINATION_AMOUNT.toString()) if (offset > 0) { urlBuilder.appendQueryParameter("offset", offset.toString()) } @@ -277,30 +277,29 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI } companion object { - // Registered under jay's MAL account - private const val clientId = "9e6656c53d1910999cc3c537e0e6256a" + private const val CLIENT_ID = "9e6656c53d1910999cc3c537e0e6256a" - private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2" - private const val baseApiUrl = "https://api.myanimelist.net/v2" + private const val BASE_OAUTH_URL = "https://myanimelist.net/v1/oauth2" + private const val BASE_API_URL = "https://api.myanimelist.net/v2" - private const val listPaginationAmount = 250 + private const val LIST_PAGINATION_AMOUNT = 250 private var codeVerifier: String = "" - fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon() - .appendQueryParameter("client_id", clientId) + fun authUrl(): Uri = "$BASE_OAUTH_URL/authorize".toUri().buildUpon() + .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("code_challenge", getPkceChallengeCode()) .appendQueryParameter("response_type", "code") .build() - fun mangaUrl(id: Long): Uri = "$baseApiUrl/manga".toUri().buildUpon() + fun mangaUrl(id: Long): Uri = "$BASE_API_URL/manga".toUri().buildUpon() .appendPath(id.toString()) .appendPath("my_list_status") .build() fun refreshTokenRequest(oauth: OAuth): Request { val formBody: RequestBody = FormBody.Builder() - .add("client_id", clientId) + .add("client_id", CLIENT_ID) .add("refresh_token", oauth.refresh_token) .add("grant_type", "refresh_token") .build() @@ -312,7 +311,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .add("Authorization", "Bearer ${oauth.access_token}") .build() - return POST("$baseOAuthUrl/token", body = formBody, headers = headers) + return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers) } private fun getPkceChallengeCode(): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 7f0c2e4bc4..8d3b8ac99f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,58 +1,40 @@ package eu.kanade.tachiyomi.data.track.myanimelist +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response -import okhttp3.internal.closeQuietly import uy.kohesive.injekt.injectLazy import java.io.IOException -class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor { +class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor { private val json: Json by injectLazy() - private var oauth: OAuth? = null + private var oauth: OAuth? = myanimelist.loadOAuth() + private val tokenExpired get() = myanimelist.getIfAuthExpired() override fun intercept(chain: Interceptor.Chain): Response { + if (tokenExpired) { + throw MALTokenExpired() + } + val originalRequest = chain.request() - if (token.isNullOrEmpty()) { - throw IOException("Not authenticated with MyAnimeList") + // Refresh access token if expired + if (oauth != null && oauth!!.isExpired()) { + setAuth(refreshToken(chain)) } + if (oauth == null) { - oauth = myanimelist.loadOAuth() - } - // Refresh access token if expired or created_at is freshly set - if (oauth != null && - (oauth!!.isExpired() || oauth!!.created_at == System.currentTimeMillis()) - ) { - val newOauth = with(json) { - runCatching { - val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) - - if (oauthResponse.isSuccessful) { - oauthResponse.parseAs() - } else { - oauthResponse.closeQuietly() - null - } - } - } - - if (newOauth.getOrNull() == null) { - throw IOException("Failed to refresh the access token") - } - - setAuth(newOauth.getOrNull()) - } - if (oauth == null) { - throw IOException("No authentication token") + throw IOException("MAL: User is not authenticated") } // Add the authorization header to the original request val authRequest = originalRequest.newBuilder() .addHeader("Authorization", "Bearer ${oauth!!.access_token}") + .header("User-Agent", "null2264/yokai/${BuildConfig.VERSION_NAME} (${BuildConfig.APPLICATION_ID})") .build() return chain.proceed(authRequest) @@ -63,8 +45,26 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t * and the oauth object. */ fun setAuth(oauth: OAuth?) { - token = oauth?.access_token this.oauth = oauth myanimelist.saveOAuth(oauth) } + + private fun refreshToken(chain: Interceptor.Chain): OAuth { + return runCatching { + val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) + if (oauthResponse.code == 401) { + myanimelist.setAuthExpired() + } + if (oauthResponse.isSuccessful) { + with(json) { oauthResponse.parseAs() } + } else { + oauthResponse.close() + null + } + } + .getOrNull() + ?: throw MALTokenExpired() + } } + +class MALTokenExpired: IOException("MAL: Login has expired")